diff --git a/Cargo.lock b/Cargo.lock index 6b6939c18b..6aae5d2890 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2249,7 +2249,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40723b8fb387abc38f4f4a37c09073622e41dd12327033091ef8950659e6dc0c" dependencies = [ "memchr", - "regex-automata 0.4.7", + "regex-automata 0.4.9", "serde", ] @@ -2800,11 +2800,29 @@ name = "cairo-lang-macro" version = "0.1.0" source = "git+https://github.com/dojoengine/scarb?rev=7eac49b3e61236ce466e712225d9c989f9db1ef3#7eac49b3e61236ce466e712225d9c989f9db1ef3" dependencies = [ - "cairo-lang-macro-attributes", + "cairo-lang-macro-attributes 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cairo-lang-macro-stable", + "linkme", +] + +[[package]] +name = "cairo-lang-macro" +version = "0.1.1" +dependencies = [ + "cairo-lang-macro-attributes 0.1.0", "cairo-lang-macro-stable", "linkme", ] +[[package]] +name = "cairo-lang-macro-attributes" +version = "0.1.0" +dependencies = [ + "quote", + "scarb-stable-hash 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 2.0.77", +] + [[package]] name = "cairo-lang-macro-attributes" version = "0.1.0" @@ -4804,7 +4822,26 @@ version = "1.0.3" dependencies = [ "cairo-lang-language-server", "clap", - "dojo-lang 1.0.3", + "dojo-macros", +] + +[[package]] +name = "dojo-macros" +version = "0.1.0" +dependencies = [ + "cairo-lang-defs", + "cairo-lang-macro 0.1.1", + "cairo-lang-parser", + "cairo-lang-plugins", + "cairo-lang-syntax", + "cairo-lang-utils", + "convert_case 0.6.0", + "dojo-types 1.0.1", + "regex", + "serde", + "serde_json", + "starknet 0.12.0", + "starknet-crypto 0.7.2", ] [[package]] @@ -6558,8 +6595,8 @@ dependencies = [ "aho-corasick", "bstr", "log", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -7423,7 +7460,7 @@ dependencies = [ "globset", "log", "memchr", - "regex-automata 0.4.7", + "regex-automata 0.4.9", "same-file", "walkdir", "winapi-util", @@ -8806,7 +8843,7 @@ dependencies = [ "petgraph", "pico-args", "regex", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "string_cache", "term", "tiny-keccak", @@ -8820,7 +8857,7 @@ version = "0.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "507460a910eb7b32ee961886ff48539633b788a36b65692b95f225b844c82553" dependencies = [ - "regex-automata 0.4.7", + "regex-automata 0.4.9", ] [[package]] @@ -11302,7 +11339,7 @@ dependencies = [ "rand 0.8.5", "rand_chacha", "rand_xorshift", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", "rusty-fork", "tempfile", "unarray", @@ -11852,14 +11889,14 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.6" +version = "1.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" dependencies = [ "aho-corasick", "memchr", - "regex-automata 0.4.7", - "regex-syntax 0.8.4", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", ] [[package]] @@ -11873,13 +11910,13 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.7" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.4", + "regex-syntax 0.8.5", ] [[package]] @@ -11890,9 +11927,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.4" +version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" [[package]] name = "relative-path" @@ -12799,7 +12836,7 @@ dependencies = [ "cairo-lang-filesystem", "cairo-lang-formatter", "cairo-lang-lowering", - "cairo-lang-macro", + "cairo-lang-macro 0.1.0", "cairo-lang-macro-stable", "cairo-lang-parser", "cairo-lang-semantic", @@ -13693,7 +13730,6 @@ dependencies = [ "clap-verbosity-flag", "colored", "dojo-bindgen", - "dojo-lang 1.0.3", "dojo-test-utils", "dojo-types 1.0.3", "dojo-utils", diff --git a/Cargo.toml b/Cargo.toml index 7aae7c7fbe..79c8817a10 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -10,7 +10,9 @@ members = [ "bin/torii", "crates/dojo/bindgen", "crates/dojo/core", +# TODO: to be removed but still used by some tools like LS "crates/dojo/lang", + "crates/dojo/macros", "crates/dojo/test-utils", "crates/dojo/types", "crates/dojo/utils", @@ -82,7 +84,7 @@ dojo-metrics = { path = "crates/metrics" } # dojo-lang dojo-bindgen = { path = "crates/dojo/bindgen" } dojo-core = { path = "crates/dojo/core" } -dojo-lang = { path = "crates/dojo/lang" } +dojo-macros = { path = "crates/dojo/macros" } dojo-test-utils = { path = "crates/dojo/test-utils" } dojo-types = { path = "crates/dojo/types" } dojo-world = { path = "crates/dojo/world" } diff --git a/bin/dojo-language-server/Cargo.toml b/bin/dojo-language-server/Cargo.toml index 8dc16e26b0..2da861cc6e 100644 --- a/bin/dojo-language-server/Cargo.toml +++ b/bin/dojo-language-server/Cargo.toml @@ -8,4 +8,4 @@ version.workspace = true [dependencies] cairo-lang-language-server.workspace = true clap.workspace = true -dojo-lang.workspace = true +dojo-macros.workspace = true diff --git a/bin/dojo-language-server/src/main.rs b/bin/dojo-language-server/src/main.rs index 7f12f22078..9d2dba72d5 100644 --- a/bin/dojo-language-server/src/main.rs +++ b/bin/dojo-language-server/src/main.rs @@ -1,6 +1,4 @@ -use cairo_lang_language_server::Tricks; use clap::Parser; -use dojo_lang::dojo_plugin_suite; /// Dojo Language Server #[derive(Parser, Debug)] @@ -8,9 +6,5 @@ use dojo_lang::dojo_plugin_suite; struct Args {} fn main() { - let _args = Args::parse(); - - let mut tricks = Tricks::default(); - tricks.extra_plugin_suites = Some(&|| vec![dojo_plugin_suite()]); - cairo_lang_language_server::start_with_tricks(tricks); + cairo_lang_language_server::start(); } diff --git a/bin/sozo/Cargo.toml b/bin/sozo/Cargo.toml index 6831fbf059..d200b5073a 100644 --- a/bin/sozo/Cargo.toml +++ b/bin/sozo/Cargo.toml @@ -23,7 +23,6 @@ clap.workspace = true clap-verbosity-flag.workspace = true colored.workspace = true dojo-bindgen.workspace = true -dojo-lang.workspace = true dojo-types.workspace = true dojo-utils.workspace = true dojo-world.workspace = true diff --git a/bin/sozo/src/commands/test.rs b/bin/sozo/src/commands/test.rs index 772a87df1a..6c18f7f213 100644 --- a/bin/sozo/src/commands/test.rs +++ b/bin/sozo/src/commands/test.rs @@ -16,7 +16,6 @@ use cairo_lang_test_plugin::{test_plugin_suite, TestsCompilationConfig}; use cairo_lang_test_runner::{CompiledTestRunner, RunProfilerConfig, TestCompiler, TestRunConfig}; use cairo_lang_utils::ordered_hash_map::OrderedHashMap; use clap::Args; -use dojo_lang::dojo_plugin_suite; use itertools::Itertools; use scarb::compiler::{ CairoCompilationUnit, CompilationUnit, CompilationUnitAttributes, ContractSelector, @@ -197,7 +196,6 @@ pub(crate) fn build_root_database(unit: &CairoCompilationUnit) -> Result ByteArray { - "test_contract" - } - } -} - -#[dojo::contract] -mod test_contract {} - -#[starknet::interface] -pub trait IQuantumLeap { - fn plz_more_tps(self: @T) -> felt252; -} - -#[starknet::contract] -pub mod test_contract_upgrade { - use dojo::contract::IContract; - use dojo::meta::IDeployedResource; - use dojo::world::IWorldDispatcher; - use dojo::contract::components::world_provider::IWorldProvider; - - #[storage] - struct Storage {} - - #[constructor] - fn constructor(ref self: ContractState) {} - - #[abi(embed_v0)] - pub impl QuantumLeap of super::IQuantumLeap { - fn plz_more_tps(self: @ContractState) -> felt252 { - 'daddy' - } - } - - #[abi(embed_v0)] - pub impl WorldProviderImpl of IWorldProvider { - fn world_dispatcher(self: @ContractState) -> IWorldDispatcher { - IWorldDispatcher { contract_address: starknet::contract_address_const::<'world'>() } - } - } - - #[abi(embed_v0)] - pub impl ContractImpl of IContract {} - - #[abi(embed_v0)] - pub impl Contract_DeployedContractImpl of IDeployedResource { - fn dojo_name(self: @ContractState) -> ByteArray { - "test_contract" - } - } -} - -#[test] -#[available_gas(7000000)] -fn test_upgrade_from_world() { - let world = deploy_world(); - let world = world.dispatcher; - - let base_address = world - .register_contract('salt', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); - let new_class_hash: ClassHash = test_contract_upgrade::TEST_CLASS_HASH.try_into().unwrap(); - - world.upgrade_contract("dojo", new_class_hash); - - let quantum_dispatcher = IQuantumLeapDispatcher { contract_address: base_address }; - assert(quantum_dispatcher.plz_more_tps() == 'daddy', 'quantum leap failed'); -} - -#[test] -#[available_gas(7000000)] -#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] -fn test_upgrade_from_world_not_world_provider() { - let world = deploy_world(); - let world = world.dispatcher; - - let _ = world - .register_contract('salt', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); - let new_class_hash: ClassHash = contract_invalid_upgrade::TEST_CLASS_HASH.try_into().unwrap(); - - world.upgrade_contract("dojo", new_class_hash); -} - -#[test] -#[available_gas(6000000)] -#[should_panic(expected: ('must be called by world', 'ENTRYPOINT_FAILED'))] -fn test_upgrade_direct() { - let world = deploy_world(); - let world = world.dispatcher; - - let base_address = world - .register_contract('salt', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); - let new_class_hash: ClassHash = test_contract_upgrade::TEST_CLASS_HASH.try_into().unwrap(); - - let upgradeable_dispatcher = IUpgradeableDispatcher { contract_address: base_address }; - upgradeable_dispatcher.upgrade(new_class_hash); -} - -#[starknet::interface] -trait IMetadataOnly { - fn dojo_name(self: @T) -> ByteArray; -} - -#[starknet::contract] -mod invalid_legacy_model { - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl InvalidModelMetadata of super::IMetadataOnly { - fn dojo_name(self: @ContractState) -> ByteArray { - "invalid_legacy_model" - } - } -} - -#[starknet::contract] -mod invalid_legacy_model_world { - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl InvalidModelName of super::IMetadataOnly { - fn dojo_name(self: @ContractState) -> ByteArray { - "invalid_legacy_model" - } - } -} - -#[starknet::contract] -mod invalid_model { - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl InvalidModelSelector of super::IMetadataOnly { - fn dojo_name(self: @ContractState) -> ByteArray { - "invalid_model" - } - } -} - -#[starknet::contract] -mod invalid_model_world { - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl InvalidModelSelector of super::IMetadataOnly { - fn dojo_name(self: @ContractState) -> ByteArray { - "invalid_model_world" - } - } -} - -#[test] -#[available_gas(6000000)] -#[should_panic( - expected: ( - "Namespace `` is invalid according to Dojo naming rules: ^[a-zA-Z0-9_]+$", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_register_namespace_empty_name() { - let world = deploy_world(); - let world = world.dispatcher; - - world.register_namespace(""); -} diff --git a/crates/dojo/core-cairo-test/src/tests/helpers/event.cairo b/crates/dojo/core-cairo-test/src/tests/helpers/event.cairo deleted file mode 100644 index 4b41eaf263..0000000000 --- a/crates/dojo/core-cairo-test/src/tests/helpers/event.cairo +++ /dev/null @@ -1,110 +0,0 @@ -use core::starknet::ContractAddress; - -use dojo::world::{IWorldDispatcher}; - -use crate::world::{spawn_test_world, NamespaceDef, TestResource}; - -/// This file contains some partial event contracts written without the dojo::event -/// attribute, to avoid having several contracts with a same name/classhash, -/// as the test runner does not differenciate them. -/// These event contracts are used to test event upgrades in tests/event.cairo. - -// This event is used as a base to create the "previous" version of an event to be upgraded. -#[derive(Introspect)] -struct FooBaseEvent { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] -pub struct FooEventBadLayoutType { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] -struct FooEventMemberRemoved { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] -struct FooEventMemberAddedButRemoved { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] -struct FooEventMemberAddedButMoved { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] -struct FooEventMemberAdded { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -pub fn deploy_world_for_event_upgrades() -> IWorldDispatcher { - let namespace_def = NamespaceDef { - namespace: "dojo", resources: [ - TestResource::Event(old_foo_event_bad_layout_type::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Event(e_FooEventMemberRemoved::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Event( - e_FooEventMemberAddedButRemoved::TEST_CLASS_HASH.try_into().unwrap() - ), - TestResource::Event(e_FooEventMemberAddedButMoved::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Event(e_FooEventMemberAdded::TEST_CLASS_HASH.try_into().unwrap()), - ].span() - }; - spawn_test_world([namespace_def].span()).dispatcher -} - -#[starknet::contract] -pub mod old_foo_event_bad_layout_type { - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl DeployedEventImpl of dojo::meta::interface::IDeployedResource { - fn dojo_name(self: @ContractState) -> ByteArray { - "FooEventBadLayoutType" - } - } - - #[abi(embed_v0)] - impl StoredImpl of dojo::meta::interface::IStoredResource { - fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { - if let dojo::meta::introspect::Ty::Struct(mut s) = - dojo::meta::introspect::Introspect::::ty() { - s.name = 'FooEventBadLayoutType'; - s - } else { - panic!("Unexpected schema.") - } - } - - fn layout(self: @ContractState) -> dojo::meta::Layout { - // Should never happen as dojo::event always derive Introspect. - dojo::meta::Layout::Fixed([].span()) - } - } -} diff --git a/crates/dojo/core-cairo-test/src/tests/helpers/model.cairo b/crates/dojo/core-cairo-test/src/tests/helpers/model.cairo deleted file mode 100644 index 238f3e4fe0..0000000000 --- a/crates/dojo/core-cairo-test/src/tests/helpers/model.cairo +++ /dev/null @@ -1,71 +0,0 @@ -use core::starknet::ContractAddress; - -use dojo::world::IWorldDispatcher; - -use crate::world::{spawn_test_world, NamespaceDef, TestResource}; - -/// This file contains some partial model contracts written without the dojo::model -/// attribute, to avoid having several contracts with a same name/classhash, -/// as the test runner does not differenciate them. -/// These model contracts are used to test model upgrades in tests/model.cairo. - -#[derive(IntrospectPacked, Copy, Drop, Serde)] -#[dojo::model] -struct FooModelBadLayoutType { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -#[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] -struct FooModelMemberRemoved { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -#[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] -struct FooModelMemberAddedButRemoved { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -#[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] -struct FooModelMemberAddedButMoved { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -#[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] -struct FooModelMemberAdded { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - - -pub fn deploy_world_for_model_upgrades() -> IWorldDispatcher { - let namespace_def = NamespaceDef { - namespace: "dojo", resources: [ - TestResource::Model(m_FooModelBadLayoutType::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Model(m_FooModelMemberRemoved::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Model( - m_FooModelMemberAddedButRemoved::TEST_CLASS_HASH.try_into().unwrap() - ), - TestResource::Model(m_FooModelMemberAddedButMoved::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Model(m_FooModelMemberAdded::TEST_CLASS_HASH.try_into().unwrap()), - ].span() - }; - spawn_test_world([namespace_def].span()).dispatcher -} diff --git a/crates/dojo/core-cairo-test/src/tests/world/event.cairo b/crates/dojo/core-cairo-test/src/tests/world/event.cairo deleted file mode 100644 index 2c52779447..0000000000 --- a/crates/dojo/core-cairo-test/src/tests/world/event.cairo +++ /dev/null @@ -1,287 +0,0 @@ -use core::starknet::ContractAddress; - -use crate::tests::helpers::{ - SimpleEvent, e_SimpleEvent, DOJO_NSH, e_FooEventBadLayoutType, drop_all_events, deploy_world, - deploy_world_for_event_upgrades -}; -use dojo::world::{world, IWorldDispatcherTrait}; -use dojo::event::Event; - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] -pub struct FooEventMemberRemoved { - #[key] - pub caller: ContractAddress, - pub b: u128, -} - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] -pub struct FooEventMemberAddedButRemoved { - #[key] - pub caller: ContractAddress, - pub b: u128, - pub c: u256, - pub d: u256 -} - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] -pub struct FooEventMemberAddedButMoved { - #[key] - pub caller: ContractAddress, - pub b: u128, - pub a: felt252, - pub c: u256 -} - -#[derive(Copy, Drop, Serde, Debug)] -#[dojo::event] -pub struct FooEventMemberAdded { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, - pub c: u256 -} - -#[test] -fn test_register_event_for_namespace_owner() { - let bob = starknet::contract_address_const::<0xb0b>(); - - let world = deploy_world(); - let world = world.dispatcher; - - world.grant_owner(DOJO_NSH, bob); - - drop_all_events(world.contract_address); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - world.register_event("dojo", e_SimpleEvent::TEST_CLASS_HASH.try_into().unwrap()); - - let event = starknet::testing::pop_log::(world.contract_address); - assert(event.is_some(), 'no event)'); - - if let world::Event::EventRegistered(event) = event.unwrap() { - assert(event.name == Event::::name(), 'bad event name'); - assert(event.namespace == "dojo", 'bad event namespace'); - assert( - event.class_hash == e_SimpleEvent::TEST_CLASS_HASH.try_into().unwrap(), - 'bad event class_hash' - ); - assert( - event.address != core::num::traits::Zero::::zero(), - 'bad event prev address' - ); - } else { - core::panic_with_felt252('no EventRegistered event'); - } - - assert(world.is_owner(Event::::selector(DOJO_NSH), bob), 'bob is not the owner'); -} - -#[test] -#[should_panic( - expected: ("Account `2827` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED',) -)] -fn test_register_event_for_namespace_writer() { - let bob = starknet::contract_address_const::<0xb0b>(); - - let world = deploy_world(); - let world = world.dispatcher; - - world.grant_writer(DOJO_NSH, bob); - - drop_all_events(world.contract_address); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - world.register_event("dojo", e_SimpleEvent::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -fn test_upgrade_event_from_event_owner() { - let bob = starknet::contract_address_const::<0xb0b>(); - - let world = deploy_world_for_event_upgrades(); - world.grant_owner(Event::::selector(DOJO_NSH), bob); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - drop_all_events(world.contract_address); - - world.upgrade_event("dojo", e_FooEventMemberAdded::TEST_CLASS_HASH.try_into().unwrap()); - - let event = starknet::testing::pop_log::(world.contract_address); - assert(event.is_some(), 'no event)'); - - if let world::Event::EventUpgraded(event) = event.unwrap() { - assert( - event.selector == Event::::selector(DOJO_NSH), 'bad model selector' - ); - assert( - event.class_hash == e_FooEventMemberAdded::TEST_CLASS_HASH.try_into().unwrap(), - 'bad model class_hash' - ); - assert( - event.address != core::num::traits::Zero::::zero(), - 'bad model prev address' - ); - } else { - core::panic_with_felt252('no EventUpgraded event'); - } - - assert( - world.is_owner(Event::::selector(DOJO_NSH), bob), - 'bob is not the owner' - ); -} - -#[test] -fn test_upgrade_event() { - let world = deploy_world_for_event_upgrades(); - - drop_all_events(world.contract_address); - - world.upgrade_event("dojo", e_FooEventMemberAdded::TEST_CLASS_HASH.try_into().unwrap()); - - let event = starknet::testing::pop_log::(world.contract_address); - assert(event.is_some(), 'no event)'); - - if let world::Event::EventUpgraded(event) = event.unwrap() { - assert( - event.selector == Event::::selector(DOJO_NSH), 'bad model selector' - ); - assert( - event.class_hash == e_FooEventMemberAdded::TEST_CLASS_HASH.try_into().unwrap(), - 'bad model class_hash' - ); - assert( - event.address != core::num::traits::Zero::::zero(), 'bad model address' - ); - } else { - core::panic_with_felt252('no EventUpgraded event'); - } -} - -#[test] -#[should_panic( - expected: ( - "Invalid new layout to upgrade the resource `dojo-FooEventBadLayoutType`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_event_with_bad_layout_type() { - let world = deploy_world_for_event_upgrades(); - world.upgrade_event("dojo", e_FooEventBadLayoutType::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic( - expected: ( - "Invalid new schema to upgrade the resource `dojo-FooEventMemberRemoved`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_event_with_member_removed() { - let world = deploy_world_for_event_upgrades(); - world.upgrade_event("dojo", e_FooEventMemberRemoved::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic( - expected: ( - "Invalid new schema to upgrade the resource `dojo-FooEventMemberAddedButRemoved`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_event_with_member_added_but_removed() { - let world = deploy_world_for_event_upgrades(); - world - .upgrade_event( - "dojo", e_FooEventMemberAddedButRemoved::TEST_CLASS_HASH.try_into().unwrap() - ); -} - -#[test] -#[should_panic( - expected: ( - "Invalid new schema to upgrade the resource `dojo-FooEventMemberAddedButMoved`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_event_with_member_moved() { - let world = deploy_world_for_event_upgrades(); - world.upgrade_event("dojo", e_FooEventMemberAddedButMoved::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic( - expected: ( - "Account `659918` does NOT have OWNER role on event (or its namespace) `FooEventMemberAdded`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_event_from_event_writer() { - let alice = starknet::contract_address_const::<0xa11ce>(); - - let world = deploy_world_for_event_upgrades(); - - world.grant_writer(Event::::selector(DOJO_NSH), alice); - - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); - world.upgrade_event("dojo", e_FooEventMemberAdded::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic( - expected: ("Resource `dojo-SimpleEvent` is already registered", 'ENTRYPOINT_FAILED',) -)] -fn test_upgrade_event_from_random_account() { - let bob = starknet::contract_address_const::<0xb0b>(); - let alice = starknet::contract_address_const::<0xa11ce>(); - - let world = deploy_world(); - let world = world.dispatcher; - - world.grant_owner(DOJO_NSH, bob); - world.grant_owner(DOJO_NSH, alice); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - world.register_event("dojo", e_SimpleEvent::TEST_CLASS_HASH.try_into().unwrap()); - - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); - world.register_event("dojo", e_SimpleEvent::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic(expected: ("Namespace `another_namespace` is not registered", 'ENTRYPOINT_FAILED',))] -fn test_register_event_with_unregistered_namespace() { - let world = deploy_world(); - let world = world.dispatcher; - - world.register_event("another_namespace", e_SimpleEvent::TEST_CLASS_HASH.try_into().unwrap()); -} - -// It's CONTRACT_NOT_DEPLOYED for now as in this example the contract is not a dojo contract -// and it's not the account that is calling the register_event function. -#[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] -fn test_register_event_through_malicious_contract() { - let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); - - let world = deploy_world(); - let world = world.dispatcher; - - world.grant_owner(DOJO_NSH, bob); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(malicious_contract); - world.register_event("dojo", e_SimpleEvent::TEST_CLASS_HASH.try_into().unwrap()); -} diff --git a/crates/dojo/core-cairo-test/src/tests/world/model.cairo b/crates/dojo/core-cairo-test/src/tests/world/model.cairo deleted file mode 100644 index 64cce8ee3e..0000000000 --- a/crates/dojo/core-cairo-test/src/tests/world/model.cairo +++ /dev/null @@ -1,309 +0,0 @@ -use core::starknet::ContractAddress; - -use crate::tests::helpers::{ - Foo, m_Foo, DOJO_NSH, drop_all_events, deploy_world, deploy_world_for_model_upgrades, - foo_invalid_name -}; -use dojo::world::{world, IWorldDispatcherTrait}; -use dojo::model::Model; - - -#[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] -pub struct FooModelBadLayoutType { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, -} - -#[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] -pub struct FooModelMemberRemoved { - #[key] - pub caller: ContractAddress, - pub b: u128, -} - -#[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] -pub struct FooModelMemberAddedButRemoved { - #[key] - pub caller: ContractAddress, - pub b: u128, - pub c: u256, - pub d: u256 -} - -#[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] -pub struct FooModelMemberAddedButMoved { - #[key] - pub caller: ContractAddress, - pub b: u128, - pub a: felt252, - pub c: u256 -} - -#[derive(Introspect, Copy, Drop, Serde)] -#[dojo::model] -pub struct FooModelMemberAdded { - #[key] - pub caller: ContractAddress, - pub a: felt252, - pub b: u128, - pub c: u256 -} - -#[test] -fn test_register_model_for_namespace_owner() { - let bob = starknet::contract_address_const::<0xb0b>(); - - let world = deploy_world(); - let world = world.dispatcher; - - world.grant_owner(DOJO_NSH, bob); - - drop_all_events(world.contract_address); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - world.register_model("dojo", m_Foo::TEST_CLASS_HASH.try_into().unwrap()); - - let event = starknet::testing::pop_log::(world.contract_address); - assert(event.is_some(), 'no event)'); - - if let world::Event::ModelRegistered(event) = event.unwrap() { - assert(event.name == Model::::name(), 'bad event name'); - assert(event.namespace == "dojo", 'bad event namespace'); - assert( - event.class_hash == m_Foo::TEST_CLASS_HASH.try_into().unwrap(), 'bad event class_hash' - ); - assert( - event.address != core::num::traits::Zero::::zero(), - 'bad event prev address' - ); - } else { - core::panic_with_felt252('no ModelRegistered event'); - } - - assert(world.is_owner(Model::::selector(DOJO_NSH), bob), 'bob is not the owner'); -} - - -#[test] -#[should_panic( - expected: ( - "Name `foo-bis` is invalid according to Dojo naming rules: ^[a-zA-Z0-9_]+$", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_register_model_with_invalid_name() { - let world = deploy_world(); - let world = world.dispatcher; - - world.register_model("dojo", foo_invalid_name::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic( - expected: ("Account `2827` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED',) -)] -fn test_register_model_for_namespace_writer() { - let bob = starknet::contract_address_const::<0xb0b>(); - - let world = deploy_world(); - let world = world.dispatcher; - - world.grant_writer(DOJO_NSH, bob); - - drop_all_events(world.contract_address); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - world.register_model("dojo", m_Foo::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -fn test_upgrade_model_from_model_owner() { - let bob = starknet::contract_address_const::<0xb0b>(); - - let world = deploy_world_for_model_upgrades(); - world.grant_owner(Model::::selector(DOJO_NSH), bob); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - - drop_all_events(world.contract_address); - - world.upgrade_model("dojo", m_FooModelMemberAdded::TEST_CLASS_HASH.try_into().unwrap()); - - let event = starknet::testing::pop_log::(world.contract_address); - assert(event.is_some(), 'no event)'); - - if let world::Event::ModelUpgraded(event) = event.unwrap() { - assert( - event.selector == Model::::selector(DOJO_NSH), 'bad model selector' - ); - assert( - event.class_hash == m_FooModelMemberAdded::TEST_CLASS_HASH.try_into().unwrap(), - 'bad model class_hash' - ); - assert( - event.address != core::num::traits::Zero::::zero(), - 'bad model prev address' - ); - } else { - core::panic_with_felt252('no ModelUpgraded event'); - } - - assert( - world.is_owner(Model::::selector(DOJO_NSH), bob), - 'bob is not the owner' - ); -} - -#[test] -fn test_upgrade_model() { - let world = deploy_world_for_model_upgrades(); - - drop_all_events(world.contract_address); - - world.upgrade_model("dojo", m_FooModelMemberAdded::TEST_CLASS_HASH.try_into().unwrap()); - - let event = starknet::testing::pop_log::(world.contract_address); - assert(event.is_some(), 'no event)'); - - if let world::Event::ModelUpgraded(event) = event.unwrap() { - assert( - event.selector == Model::::selector(DOJO_NSH), 'bad model selector' - ); - assert( - event.class_hash == m_FooModelMemberAdded::TEST_CLASS_HASH.try_into().unwrap(), - 'bad model class_hash' - ); - assert( - event.address != core::num::traits::Zero::::zero(), 'bad model address' - ); - } else { - core::panic_with_felt252('no ModelUpgraded event'); - } -} - -#[test] -#[should_panic( - expected: ( - "Invalid new layout to upgrade the resource `dojo-FooModelBadLayoutType`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_model_with_bad_layout_type() { - let world = deploy_world_for_model_upgrades(); - world.upgrade_model("dojo", m_FooModelBadLayoutType::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic( - expected: ( - "Invalid new schema to upgrade the resource `dojo-FooModelMemberRemoved`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_model_with_member_removed() { - let world = deploy_world_for_model_upgrades(); - world.upgrade_model("dojo", m_FooModelMemberRemoved::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic( - expected: ( - "Invalid new schema to upgrade the resource `dojo-FooModelMemberAddedButRemoved`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_model_with_member_added_but_removed() { - let world = deploy_world_for_model_upgrades(); - world - .upgrade_model( - "dojo", m_FooModelMemberAddedButRemoved::TEST_CLASS_HASH.try_into().unwrap() - ); -} - -#[test] -#[should_panic( - expected: ( - "Invalid new schema to upgrade the resource `dojo-FooModelMemberAddedButMoved`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_model_with_member_moved() { - let world = deploy_world_for_model_upgrades(); - world.upgrade_model("dojo", m_FooModelMemberAddedButMoved::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic( - expected: ( - "Account `659918` does NOT have OWNER role on model (or its namespace) `FooModelMemberAdded`", - 'ENTRYPOINT_FAILED', - ) -)] -fn test_upgrade_model_from_model_writer() { - let alice = starknet::contract_address_const::<0xa11ce>(); - - let world = deploy_world_for_model_upgrades(); - - world.grant_writer(Model::::selector(DOJO_NSH), alice); - - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); - world.upgrade_model("dojo", m_FooModelMemberAdded::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic(expected: ("Resource `dojo-Foo` is already registered", 'ENTRYPOINT_FAILED',))] -fn test_upgrade_model_from_random_account() { - let bob = starknet::contract_address_const::<0xb0b>(); - let alice = starknet::contract_address_const::<0xa11ce>(); - - let world = deploy_world(); - let world = world.dispatcher; - - world.grant_owner(DOJO_NSH, bob); - world.grant_owner(DOJO_NSH, alice); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); - world.register_model("dojo", m_Foo::TEST_CLASS_HASH.try_into().unwrap()); - - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); - world.register_model("dojo", m_Foo::TEST_CLASS_HASH.try_into().unwrap()); -} - -#[test] -#[should_panic(expected: ("Namespace `another_namespace` is not registered", 'ENTRYPOINT_FAILED',))] -fn test_register_model_with_unregistered_namespace() { - let world = deploy_world(); - let world = world.dispatcher; - - world.register_model("another_namespace", m_Foo::TEST_CLASS_HASH.try_into().unwrap()); -} - -// It's CONTRACT_NOT_DEPLOYED for now as in this example the contract is not a dojo contract -// and it's not the account that is calling the register_model function. -#[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] -fn test_register_model_through_malicious_contract() { - let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); - - let world = deploy_world(); - let world = world.dispatcher; - - world.grant_owner(DOJO_NSH, bob); - - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(malicious_contract); - world.register_model("dojo", m_Foo::TEST_CLASS_HASH.try_into().unwrap()); -} diff --git a/crates/dojo/core-foundry-test/.snfoundry_cache/.prev_tests_failed b/crates/dojo/core-foundry-test/.snfoundry_cache/.prev_tests_failed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/crates/dojo/core-foundry-test/Scarb.lock b/crates/dojo/core-foundry-test/Scarb.lock new file mode 100644 index 0000000000..854f808001 --- /dev/null +++ b/crates/dojo/core-foundry-test/Scarb.lock @@ -0,0 +1,36 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "dojo" +version = "1.0.1" +dependencies = [ + "dojo_macros", +] + +[[package]] +name = "dojo_foundry_test" +version = "1.0.0-rc.0" +dependencies = [ + "dojo", + "snforge_std", +] + +[[package]] +name = "dojo_macros" +version = "0.1.0" + +[[package]] +name = "snforge_scarb_plugin" +version = "0.33.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:b4dd6088372decd367652827091e0589bbf6bc550dfc3957baa3e9c61d6eb449" + +[[package]] +name = "snforge_std" +version = "0.33.0" +source = "registry+https://scarbs.xyz/" +checksum = "sha256:f7dc3349f8a6ef4915c93df447a00bd5a53a31129fd0990a00afa0ad31d91b06" +dependencies = [ + "snforge_scarb_plugin", +] diff --git a/crates/dojo/core-cairo-test/Scarb.toml b/crates/dojo/core-foundry-test/Scarb.toml similarity index 53% rename from crates/dojo/core-cairo-test/Scarb.toml rename to crates/dojo/core-foundry-test/Scarb.toml index b7e2111a73..9e271ba8a1 100644 --- a/crates/dojo/core-cairo-test/Scarb.toml +++ b/crates/dojo/core-foundry-test/Scarb.toml @@ -2,7 +2,7 @@ cairo-version = "=2.8.4" edition = "2024_07" description = "Testing library for Dojo using Cairo test runner." -name = "dojo_cairo_test" +name = "dojo_foundry_test" version = "1.0.0-rc.0" [dependencies] @@ -10,6 +10,13 @@ starknet = "=2.8.4" dojo = { path = "../core" } [dev-dependencies] -cairo_test = "=2.8.4" +snforge_std = "0.33.0" +assert_macros = "2.8.4" + +[scripts] +test = "snforge test" [lib] + +[[target.starknet-contract]] +build-external-contracts = ["dojo::world::world_contract::world"] diff --git a/crates/dojo/core-cairo-test/src/lib.cairo b/crates/dojo/core-foundry-test/src/lib.cairo similarity index 61% rename from crates/dojo/core-cairo-test/src/lib.cairo rename to crates/dojo/core-foundry-test/src/lib.cairo index 7517f85da7..4530fc5ba6 100644 --- a/crates/dojo/core-cairo-test/src/lib.cairo +++ b/crates/dojo/core-foundry-test/src/lib.cairo @@ -1,16 +1,16 @@ -//! Testing library for Dojo using Cairo test runner. - -#[cfg(target: "test")] +#[cfg(test)] mod utils; -#[cfg(target: "test")] +#[cfg(test)] +mod snf_utils; +#[cfg(test)] mod world; -#[cfg(target: "test")] +#[cfg(test)] pub use utils::{GasCounter, assert_array, GasCounterTrait}; -#[cfg(target: "test")] +#[cfg(test)] pub use world::{ - deploy_contract, deploy_with_world_address, spawn_test_world, NamespaceDef, TestResource, - ContractDef, ContractDefTrait, WorldStorageTestTrait, + spawn_test_world, NamespaceDef, TestResource, ContractDef, ContractDefTrait, + WorldStorageTestTrait, }; #[cfg(test)] @@ -33,7 +33,6 @@ mod tests { mod storage; } - mod contract; // mod benchmarks; mod expanded { @@ -43,17 +42,15 @@ mod tests { mod helpers { mod helpers; pub use helpers::{ - DOJO_NSH, SimpleEvent, e_SimpleEvent, Foo, m_Foo, foo_invalid_name, foo_setter, + DOJO_NSH, SimpleEvent, e_SimpleEvent, Foo, m_Foo, m_FooInvalidName, foo_setter, test_contract, test_contract_with_dojo_init_args, Sword, Case, Character, Abilities, Stats, Weapon, Ibar, IbarDispatcher, IbarDispatcherTrait, bar, deploy_world, - deploy_world_and_bar, deploy_world_and_foo, drop_all_events, IFooSetter, - IFooSetterDispatcher, IFooSetterDispatcherTrait, NotCopiable + deploy_world_and_bar, deploy_world_and_foo, IFooSetter, IFooSetterDispatcher, + IFooSetterDispatcherTrait, NotCopiable, malicious_contract }; mod event; - pub use event::{ - FooEventBadLayoutType, e_FooEventBadLayoutType, deploy_world_for_event_upgrades - }; + pub use event::deploy_world_for_event_upgrades; mod model; pub use model::deploy_world_for_model_upgrades; diff --git a/crates/dojo/core-foundry-test/src/snf_utils.cairo b/crates/dojo/core-foundry-test/src/snf_utils.cairo new file mode 100644 index 0000000000..f110318754 --- /dev/null +++ b/crates/dojo/core-foundry-test/src/snf_utils.cairo @@ -0,0 +1,40 @@ +use starknet::{ClassHash, ContractAddress}; +use snforge_std::{ContractClassTrait, DeclareResultTrait}; +use snforge_std::cheatcodes::contract_class::ContractClass; + +pub fn declare(name: ByteArray) -> (ContractClass, ClassHash) { + let contract = snforge_std::declare(name).unwrap().contract_class(); + (*contract, (*contract.class_hash).into()) +} + +pub fn deploy(contract: ContractClass, calldata: @Array) -> ContractAddress { + let (address, _) = contract.deploy(calldata).unwrap(); + address +} + +pub fn declare_and_deploy(name: ByteArray) -> ContractAddress { + let contract = snforge_std::declare(name).unwrap().contract_class(); + let (address, _) = contract.deploy(@array![]).unwrap(); + address +} + +pub fn declare_contract(name: ByteArray) -> ClassHash { + let (_, class_hash) = declare(name.clone()); + class_hash +} + +pub fn declare_event_contract(name: ByteArray) -> ClassHash { + declare_contract(format!("e_{name}")) +} + +pub fn declare_model_contract(name: ByteArray) -> ClassHash { + declare_contract(format!("m_{name}")) +} + +pub fn set_account_address(account: ContractAddress) { + snforge_std::start_cheat_account_contract_address_global(account); +} + +pub fn set_caller_address(contract: ContractAddress) { + snforge_std::start_cheat_caller_address_global(contract); +} diff --git a/crates/dojo/core-cairo-test/src/tests/benchmarks.cairo b/crates/dojo/core-foundry-test/src/tests/benchmarks.cairo similarity index 98% rename from crates/dojo/core-cairo-test/src/tests/benchmarks.cairo rename to crates/dojo/core-foundry-test/src/tests/benchmarks.cairo index e76b91b479..4196764a1e 100644 --- a/crates/dojo/core-cairo-test/src/tests/benchmarks.cairo +++ b/crates/dojo/core-foundry-test/src/tests/benchmarks.cairo @@ -45,9 +45,9 @@ struct ComplexModel { fn deploy_world() -> IWorldDispatcher { let namespace_def = NamespaceDef { namespace: "dojo", resources: [ - TestResource::Model(case::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Model(case_not_packed::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Model(complex_model::TEST_CLASS_HASH.try_into().unwrap()), + TestResource::Model("Case"), + TestResource::Model("CaseNotPacked"), + TestResource::Model("ComplexM"), ].span(), }; diff --git a/crates/dojo/core-cairo-test/src/tests/event/event.cairo b/crates/dojo/core-foundry-test/src/tests/event/event.cairo similarity index 100% rename from crates/dojo/core-cairo-test/src/tests/event/event.cairo rename to crates/dojo/core-foundry-test/src/tests/event/event.cairo diff --git a/crates/dojo/core-cairo-test/src/tests/expanded/selector_attack.cairo b/crates/dojo/core-foundry-test/src/tests/expanded/selector_attack.cairo similarity index 100% rename from crates/dojo/core-cairo-test/src/tests/expanded/selector_attack.cairo rename to crates/dojo/core-foundry-test/src/tests/expanded/selector_attack.cairo diff --git a/crates/dojo/core-foundry-test/src/tests/helpers/event.cairo b/crates/dojo/core-foundry-test/src/tests/helpers/event.cairo new file mode 100644 index 0000000000..f5e4d44928 --- /dev/null +++ b/crates/dojo/core-foundry-test/src/tests/helpers/event.cairo @@ -0,0 +1,183 @@ +use core::starknet::ContractAddress; + +use dojo::world::{IWorldDispatcher}; + +use crate::world::{spawn_test_world, NamespaceDef, TestResource}; + +// This event is used as a base to create the "previous" version of an event to be upgraded. +#[derive(Introspect)] +struct FooBaseEvent { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +pub fn deploy_world_for_event_upgrades() -> IWorldDispatcher { + let namespace_def = NamespaceDef { + namespace: "dojo", resources: [ + TestResource::Event("OldFooEventBadLayoutType"), + TestResource::Event("OldFooEventMemberRemoved"), + TestResource::Event("OldFooEventMemberAddedButRemoved"), + TestResource::Event("OldFooEventMemberAddedButMoved"), + TestResource::Event("OldFooEventMemberAdded"), + ].span() + }; + spawn_test_world([namespace_def].span()).dispatcher +} + +/// This file contains some partial event contracts written without the dojo::event +/// attribute, to avoid having several contracts with a same name, +/// as the snfoundry test runner does not differenciate them. +/// These event contracts are used to test event upgrades in tests/event.cairo. + +#[starknet::contract] +pub mod e_OldFooEventBadLayoutType { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl DeployedEventImpl of dojo::meta::interface::IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "FooEventBadLayoutType" + } + } + + #[abi(embed_v0)] + impl StoredImpl of dojo::meta::interface::IStoredResource { + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(mut s) = + dojo::meta::introspect::Introspect::::ty() { + s.name = 'FooEventBadLayoutType'; + s + } else { + panic!("Unexpected schema.") + } + } + + fn layout(self: @ContractState) -> dojo::meta::Layout { + // Should never happen as dojo::event always derive Introspect. + dojo::meta::Layout::Fixed([].span()) + } + } +} + +#[starknet::contract] +pub mod e_OldFooEventMemberRemoved { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl DeployedEventImpl of dojo::meta::interface::IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "FooEventMemberRemoved" + } + } + + #[abi(embed_v0)] + impl StoredImpl of dojo::meta::interface::IStoredResource { + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(mut s) = + dojo::meta::introspect::Introspect::::ty() { + s.name = 'FooEventMemberRemoved'; + s + } else { + panic!("Unexpected schema.") + } + } + + fn layout(self: @ContractState) -> dojo::meta::Layout { + dojo::meta::introspect::Introspect::::layout() + } + } +} + +#[starknet::contract] +pub mod e_OldFooEventMemberAddedButRemoved { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl DeployedEventImpl of dojo::meta::interface::IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "FooEventMemberAddedButRemoved" + } + } + + #[abi(embed_v0)] + impl StoredImpl of dojo::meta::interface::IStoredResource { + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(mut s) = + dojo::meta::introspect::Introspect::::ty() { + s.name = 'FooEventMemberAddedButRemoved'; + s + } else { + panic!("Unexpected schema.") + } + } + + fn layout(self: @ContractState) -> dojo::meta::Layout { + dojo::meta::introspect::Introspect::::layout() + } + } +} + +#[starknet::contract] +pub mod e_OldFooEventMemberAddedButMoved { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl DeployedEventImpl of dojo::meta::interface::IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "FooEventMemberAddedButMoved" + } + } + + #[abi(embed_v0)] + impl StoredImpl of dojo::meta::interface::IStoredResource { + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(mut s) = + dojo::meta::introspect::Introspect::::ty() { + s.name = 'FooEventMemberAddedButMoved'; + s + } else { + panic!("Unexpected schema.") + } + } + + fn layout(self: @ContractState) -> dojo::meta::Layout { + dojo::meta::introspect::Introspect::::layout() + } + } +} + +#[starknet::contract] +pub mod e_OldFooEventMemberAdded { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl DeployedEventImpl of dojo::meta::interface::IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "FooEventMemberAdded" + } + } + + #[abi(embed_v0)] + impl StoredImpl of dojo::meta::interface::IStoredResource { + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(mut s) = + dojo::meta::introspect::Introspect::::ty() { + s.name = 'FooEventMemberAdded'; + s + } else { + panic!("Unexpected schema.") + } + } + + fn layout(self: @ContractState) -> dojo::meta::Layout { + dojo::meta::introspect::Introspect::::layout() + } + } +} diff --git a/crates/dojo/core-cairo-test/src/tests/helpers/helpers.cairo b/crates/dojo/core-foundry-test/src/tests/helpers/helpers.cairo similarity index 92% rename from crates/dojo/core-cairo-test/src/tests/helpers/helpers.cairo rename to crates/dojo/core-foundry-test/src/tests/helpers/helpers.cairo index f0e859ec86..3f194826e8 100644 --- a/crates/dojo/core-cairo-test/src/tests/helpers/helpers.cairo +++ b/crates/dojo/core-foundry-test/src/tests/helpers/helpers.cairo @@ -36,7 +36,7 @@ pub struct NotCopiable { } #[starknet::contract] -pub mod foo_invalid_name { +pub mod m_FooInvalidName { use dojo::model::IModel; #[storage] @@ -101,9 +101,21 @@ pub mod foo_setter { } } +#[dojo::contract] +pub mod dojo_caller_contract {} + +#[starknet::contract] +pub mod non_dojo_caller_contract { + #[storage] + struct Storage {} +} + #[dojo::contract] pub mod test_contract {} +#[dojo::contract] +pub mod another_test_contract {} + #[dojo::contract] pub mod test_contract_with_dojo_init_args { fn dojo_init(ref self: ContractState, arg1: felt252) { @@ -203,6 +215,12 @@ pub mod bar { } } +#[starknet::contract] +pub mod malicious_contract { + #[storage] + struct Storage {} +} + /// Deploys an empty world with the `dojo` namespace. pub fn deploy_world() -> WorldStorage { let namespace_def = NamespaceDef { namespace: "dojo", resources: [].span(), }; @@ -215,8 +233,7 @@ pub fn deploy_world() -> WorldStorage { pub fn deploy_world_and_foo() -> (WorldStorage, felt252) { let namespace_def = NamespaceDef { namespace: "dojo", resources: [ - TestResource::Model(m_Foo::TEST_CLASS_HASH), - TestResource::Model(m_NotCopiable::TEST_CLASS_HASH), + TestResource::Model("Foo"), TestResource::Model("NotCopiable"), ].span(), }; @@ -228,8 +245,7 @@ pub fn deploy_world_and_foo() -> (WorldStorage, felt252) { pub fn deploy_world_and_bar() -> (WorldStorage, IbarDispatcher) { let namespace_def = NamespaceDef { namespace: "dojo", resources: [ - TestResource::Model(m_Foo::TEST_CLASS_HASH), - TestResource::Contract(bar::TEST_CLASS_HASH), + TestResource::Model("Foo"), TestResource::Contract("bar"), ].span(), }; @@ -245,11 +261,3 @@ pub fn deploy_world_and_bar() -> (WorldStorage, IbarDispatcher) { (world, bar_contract) } -pub fn drop_all_events(address: ContractAddress) { - loop { - match starknet::testing::pop_log_raw(address) { - core::option::Option::Some(_) => {}, - core::option::Option::None => { break; }, - }; - } -} diff --git a/crates/dojo/core-foundry-test/src/tests/helpers/model.cairo b/crates/dojo/core-foundry-test/src/tests/helpers/model.cairo new file mode 100644 index 0000000000..1d97a543fb --- /dev/null +++ b/crates/dojo/core-foundry-test/src/tests/helpers/model.cairo @@ -0,0 +1,183 @@ +use core::starknet::ContractAddress; + +use dojo::world::IWorldDispatcher; + +use crate::world::{spawn_test_world, NamespaceDef, TestResource}; + +// This model is used as a base to create the "previous" version of a model to be upgraded. +#[derive(Introspect)] +struct FooBaseModel { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +pub fn deploy_world_for_model_upgrades() -> IWorldDispatcher { + let namespace_def = NamespaceDef { + namespace: "dojo", resources: [ + TestResource::Model("OldFooModelBadLayoutType"), + TestResource::Model("OldFooModelMemberRemoved"), + TestResource::Model("OldFooModelMemberAddedButRemoved"), + TestResource::Model("OldFooModelMemberAddedButMoved"), + TestResource::Model("OldFooModelMemberAdded"), + ].span() + }; + spawn_test_world([namespace_def].span()).dispatcher +} + +/// This file contains some partial model contracts written without the dojo::model +/// attribute, to avoid having several contracts with a same name, +/// as the snfoundry test runner does not differenciate them. +/// These model contracts are used to test model upgrades in tests/model.cairo. + +#[starknet::contract] +pub mod m_OldFooModelBadLayoutType { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl DeployedModelImpl of dojo::meta::interface::IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "FooModelBadLayoutType" + } + } + + #[abi(embed_v0)] + impl StoredImpl of dojo::meta::interface::IStoredResource { + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(mut s) = + dojo::meta::introspect::Introspect::::ty() { + s.name = 'FooModelBadLayoutType'; + s + } else { + panic!("Unexpected schema.") + } + } + + fn layout(self: @ContractState) -> dojo::meta::Layout { + // Should never happen as dojo::model always derive Introspect. + dojo::meta::Layout::Fixed([].span()) + } + } +} + +#[starknet::contract] +pub mod m_OldFooModelMemberRemoved { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl DeployedModelImpl of dojo::meta::interface::IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "FooModelMemberRemoved" + } + } + + #[abi(embed_v0)] + impl StoredImpl of dojo::meta::interface::IStoredResource { + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(mut s) = + dojo::meta::introspect::Introspect::::ty() { + s.name = 'FooModelMemberRemoved'; + s + } else { + panic!("Unexpected schema.") + } + } + + fn layout(self: @ContractState) -> dojo::meta::Layout { + dojo::meta::introspect::Introspect::::layout() + } + } +} + +#[starknet::contract] +pub mod m_OldFooModelMemberAddedButRemoved { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl DeployedModelImpl of dojo::meta::interface::IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "FooModelMemberAddedButRemoved" + } + } + + #[abi(embed_v0)] + impl StoredImpl of dojo::meta::interface::IStoredResource { + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(mut s) = + dojo::meta::introspect::Introspect::::ty() { + s.name = 'FooModelMemberAddedButRemoved'; + s + } else { + panic!("Unexpected schema.") + } + } + + fn layout(self: @ContractState) -> dojo::meta::Layout { + dojo::meta::introspect::Introspect::::layout() + } + } +} + +#[starknet::contract] +pub mod m_OldFooModelMemberAddedButMoved { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl DeployedModelImpl of dojo::meta::interface::IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "FooModelMemberAddedButMoved" + } + } + + #[abi(embed_v0)] + impl StoredImpl of dojo::meta::interface::IStoredResource { + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(mut s) = + dojo::meta::introspect::Introspect::::ty() { + s.name = 'FooModelMemberAddedButMoved'; + s + } else { + panic!("Unexpected schema.") + } + } + + fn layout(self: @ContractState) -> dojo::meta::Layout { + dojo::meta::introspect::Introspect::::layout() + } + } +} + +#[starknet::contract] +pub mod m_OldFooModelMemberAdded { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl DeployedModelImpl of dojo::meta::interface::IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "FooModelMemberAdded" + } + } + + #[abi(embed_v0)] + impl StoredImpl of dojo::meta::interface::IStoredResource { + fn schema(self: @ContractState) -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(mut s) = + dojo::meta::introspect::Introspect::::ty() { + s.name = 'FooModelMemberAdded'; + s + } else { + panic!("Unexpected schema.") + } + } + + fn layout(self: @ContractState) -> dojo::meta::Layout { + dojo::meta::introspect::Introspect::::layout() + } + } +} diff --git a/crates/dojo/core-cairo-test/src/tests/meta/introspect.cairo b/crates/dojo/core-foundry-test/src/tests/meta/introspect.cairo similarity index 97% rename from crates/dojo/core-cairo-test/src/tests/meta/introspect.cairo rename to crates/dojo/core-foundry-test/src/tests/meta/introspect.cairo index bfa89e3023..6f994f5521 100644 --- a/crates/dojo/core-cairo-test/src/tests/meta/introspect.cairo +++ b/crates/dojo/core-foundry-test/src/tests/meta/introspect.cairo @@ -281,7 +281,7 @@ fn test_layout_of_inner_packed_struct() { } #[test] -#[should_panic(expected: ("A packed model layout must contain Fixed layouts only.",))] +#[should_panic(expected: "A packed model layout must contain Fixed layouts only.")] fn test_layout_of_not_packed_inner_struct() { let _ = Introspect::::layout(); } @@ -304,7 +304,7 @@ fn test_layout_of_inner_packed_enum() { } #[test] -#[should_panic(expected: ("A packed model layout must contain Fixed layouts only.",))] +#[should_panic(expected: "A packed model layout must contain Fixed layouts only.")] fn test_layout_of_not_packed_inner_enum() { let _ = Introspect::::layout(); } diff --git a/crates/dojo/core-cairo-test/src/tests/model/model.cairo b/crates/dojo/core-foundry-test/src/tests/model/model.cairo similarity index 95% rename from crates/dojo/core-cairo-test/src/tests/model/model.cairo rename to crates/dojo/core-foundry-test/src/tests/model/model.cairo index ff48b33192..7be2ec8c45 100644 --- a/crates/dojo/core-cairo-test/src/tests/model/model.cairo +++ b/crates/dojo/core-foundry-test/src/tests/model/model.cairo @@ -1,6 +1,6 @@ use dojo::model::{Model, ModelValue, ModelStorage, ModelValueStorage}; use dojo::world::WorldStorage; -use dojo_cairo_test::{spawn_test_world, NamespaceDef, TestResource}; +use crate::world::{spawn_test_world, NamespaceDef, TestResource}; #[derive(Copy, Drop, Serde, Debug)] #[dojo::model] @@ -28,8 +28,7 @@ struct Foo2 { fn namespace_def() -> NamespaceDef { NamespaceDef { namespace: "dojo_cairo_test", resources: [ - TestResource::Model(m_Foo::TEST_CLASS_HASH.try_into().unwrap()), - TestResource::Model(m_Foo2::TEST_CLASS_HASH.try_into().unwrap()), + TestResource::Model("Foo"), TestResource::Model("Foo2"), ].span() } } diff --git a/crates/dojo/core-cairo-test/src/tests/storage/database.cairo b/crates/dojo/core-foundry-test/src/tests/storage/database.cairo similarity index 100% rename from crates/dojo/core-cairo-test/src/tests/storage/database.cairo rename to crates/dojo/core-foundry-test/src/tests/storage/database.cairo diff --git a/crates/dojo/core-cairo-test/src/tests/storage/packing.cairo b/crates/dojo/core-foundry-test/src/tests/storage/packing.cairo similarity index 100% rename from crates/dojo/core-cairo-test/src/tests/storage/packing.cairo rename to crates/dojo/core-foundry-test/src/tests/storage/packing.cairo diff --git a/crates/dojo/core-cairo-test/src/tests/storage/storage.cairo b/crates/dojo/core-foundry-test/src/tests/storage/storage.cairo similarity index 100% rename from crates/dojo/core-cairo-test/src/tests/storage/storage.cairo rename to crates/dojo/core-foundry-test/src/tests/storage/storage.cairo diff --git a/crates/dojo/core-cairo-test/src/tests/utils/hash.cairo b/crates/dojo/core-foundry-test/src/tests/utils/hash.cairo similarity index 100% rename from crates/dojo/core-cairo-test/src/tests/utils/hash.cairo rename to crates/dojo/core-foundry-test/src/tests/utils/hash.cairo diff --git a/crates/dojo/core-cairo-test/src/tests/utils/key.cairo b/crates/dojo/core-foundry-test/src/tests/utils/key.cairo similarity index 100% rename from crates/dojo/core-cairo-test/src/tests/utils/key.cairo rename to crates/dojo/core-foundry-test/src/tests/utils/key.cairo diff --git a/crates/dojo/core-cairo-test/src/tests/utils/layout.cairo b/crates/dojo/core-foundry-test/src/tests/utils/layout.cairo similarity index 100% rename from crates/dojo/core-cairo-test/src/tests/utils/layout.cairo rename to crates/dojo/core-foundry-test/src/tests/utils/layout.cairo diff --git a/crates/dojo/core-cairo-test/src/tests/utils/misc.cairo b/crates/dojo/core-foundry-test/src/tests/utils/misc.cairo similarity index 100% rename from crates/dojo/core-cairo-test/src/tests/utils/misc.cairo rename to crates/dojo/core-foundry-test/src/tests/utils/misc.cairo diff --git a/crates/dojo/core-cairo-test/src/tests/utils/naming.cairo b/crates/dojo/core-foundry-test/src/tests/utils/naming.cairo similarity index 100% rename from crates/dojo/core-cairo-test/src/tests/utils/naming.cairo rename to crates/dojo/core-foundry-test/src/tests/utils/naming.cairo diff --git a/crates/dojo/core-cairo-test/src/tests/world/acl.cairo b/crates/dojo/core-foundry-test/src/tests/world/acl.cairo similarity index 55% rename from crates/dojo/core-cairo-test/src/tests/world/acl.cairo rename to crates/dojo/core-foundry-test/src/tests/world/acl.cairo index f899b51dfb..9f46389ab8 100644 --- a/crates/dojo/core-cairo-test/src/tests/world/acl.cairo +++ b/crates/dojo/core-foundry-test/src/tests/world/acl.cairo @@ -1,39 +1,44 @@ -use dojo::utils::bytearray_hash; use dojo::world::IWorldDispatcherTrait; use crate::tests::helpers::{ - deploy_world, foo_setter, IFooSetterDispatcher, IFooSetterDispatcherTrait, deploy_world_and_foo + deploy_world, IFooSetterDispatcher, IFooSetterDispatcherTrait, deploy_world_and_foo }; -use crate::tests::expanded::selector_attack::{attacker_model, attacker_contract}; +use crate::snf_utils; + #[test] fn test_owner() { - let (world, foo_selector) = deploy_world_and_foo(); + // deploy a dedicated contract to be used as caller/account address because of + // the way `world.panic_with_details()` is written. + // Once this function will use SRC5, we will be able to remove these lines + let caller_contract = snf_utils::declare_and_deploy("dojo_caller_contract"); + snf_utils::set_caller_address(caller_contract); + snf_utils::set_account_address(caller_contract); + let (world, foo_selector) = deploy_world_and_foo(); let world = world.dispatcher; - let alice = starknet::contract_address_const::<0xa11ce>(); - let bob = starknet::contract_address_const::<0xb0b>(); + let test_contract = snf_utils::declare_and_deploy("test_contract"); + let another_test_contract = snf_utils::declare_and_deploy("another_test_contract"); - assert(!world.is_owner(0, alice), 'should not be owner'); - assert(!world.is_owner(foo_selector, bob), 'should not be owner'); + assert(!world.is_owner(0, test_contract), 'should not be owner'); + assert(!world.is_owner(foo_selector, another_test_contract), 'should not be owner'); + world.grant_owner(0, test_contract); + assert(world.is_owner(0, test_contract), 'should be owner'); - world.grant_owner(0, alice); - assert(world.is_owner(0, alice), 'should be owner'); + world.grant_owner(foo_selector, another_test_contract); + assert(world.is_owner(foo_selector, another_test_contract), 'should be owner'); - world.grant_owner(foo_selector, bob); - assert(world.is_owner(foo_selector, bob), 'should be owner'); + world.revoke_owner(0, test_contract); + assert(!world.is_owner(0, test_contract), 'should not be owner'); - world.revoke_owner(0, alice); - assert(!world.is_owner(0, alice), 'should not be owner'); - - world.revoke_owner(foo_selector, bob); - assert(!world.is_owner(foo_selector, bob), 'should not be owner'); + world.revoke_owner(foo_selector, another_test_contract); + assert(!world.is_owner(foo_selector, another_test_contract), 'should not be owner'); } #[test] -#[should_panic(expected: ("Resource `42` is not registered", 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: "Resource `42` is not registered")] fn test_grant_owner_not_registered_resource() { let world = deploy_world(); let world = world.dispatcher; @@ -43,29 +48,26 @@ fn test_grant_owner_not_registered_resource() { } #[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] fn test_grant_owner_through_malicious_contract() { let (world, foo_selector) = deploy_world_and_foo(); let world = world.dispatcher; let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); + let malicious_contract = snf_utils::declare_and_deploy("malicious_contract"); world.grant_owner(foo_selector, alice); - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(malicious_contract); + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(malicious_contract); world.grant_owner(foo_selector, bob); } #[test] #[should_panic( - expected: ( - "Account `659918` does NOT have OWNER role on model (or its namespace) `Foo`", - 'ENTRYPOINT_FAILED' - ) + expected: "Account `659918` does NOT have OWNER role on model (or its namespace) `Foo`" )] fn test_grant_owner_fails_for_non_owner() { let (world, foo_selector) = deploy_world_and_foo(); @@ -74,37 +76,34 @@ fn test_grant_owner_fails_for_non_owner() { let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(alice); world.grant_owner(foo_selector, bob); } #[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] fn test_revoke_owner_through_malicious_contract() { let (world, foo_selector) = deploy_world_and_foo(); let world = world.dispatcher; let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); + let malicious_contract = snf_utils::declare_and_deploy("malicious_contract"); world.grant_owner(foo_selector, alice); world.grant_owner(foo_selector, bob); - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(malicious_contract); + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(malicious_contract); world.revoke_owner(foo_selector, bob); } #[test] #[should_panic( - expected: ( - "Account `659918` does NOT have OWNER role on model (or its namespace) `Foo`", - 'ENTRYPOINT_FAILED' - ) + expected: "Account `659918` does NOT have OWNER role on model (or its namespace) `Foo`" )] fn test_revoke_owner_fails_for_non_owner() { let (world, foo_selector) = deploy_world_and_foo(); @@ -115,8 +114,8 @@ fn test_revoke_owner_fails_for_non_owner() { world.grant_owner(foo_selector, bob); - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(alice); world.revoke_owner(foo_selector, bob); } @@ -146,29 +145,26 @@ fn test_writer_not_registered_resource() { } #[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] fn test_grant_writer_through_malicious_contract() { let (world, foo_selector) = deploy_world_and_foo(); let world = world.dispatcher; let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); + let malicious_contract = snf_utils::declare_and_deploy("malicious_contract"); world.grant_owner(foo_selector, alice); - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(malicious_contract); + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(malicious_contract); world.grant_writer(foo_selector, bob); } #[test] #[should_panic( - expected: ( - "Account `659918` does NOT have OWNER role on model (or its namespace) `Foo`", - 'ENTRYPOINT_FAILED' - ) + expected: "Account `659918` does NOT have OWNER role on model (or its namespace) `Foo`" )] fn test_grant_writer_fails_for_non_owner() { let (world, foo_selector) = deploy_world_and_foo(); @@ -177,37 +173,34 @@ fn test_grant_writer_fails_for_non_owner() { let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(alice); world.grant_writer(foo_selector, bob); } #[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] fn test_revoke_writer_through_malicious_contract() { let (world, foo_selector) = deploy_world_and_foo(); let world = world.dispatcher; let alice = starknet::contract_address_const::<0xa11ce>(); let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); + let malicious_contract = snf_utils::declare_and_deploy("malicious_contract"); world.grant_owner(foo_selector, alice); world.grant_writer(foo_selector, bob); - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(malicious_contract); + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(malicious_contract); world.revoke_writer(foo_selector, bob); } #[test] #[should_panic( - expected: ( - "Account `659918` does NOT have OWNER role on model (or its namespace) `Foo`", - 'ENTRYPOINT_FAILED' - ) + expected: "Account `659918` does NOT have OWNER role on model (or its namespace) `Foo`" )] fn test_revoke_writer_fails_for_non_owner() { let (world, foo_selector) = deploy_world_and_foo(); @@ -218,58 +211,39 @@ fn test_revoke_writer_fails_for_non_owner() { world.grant_owner(foo_selector, bob); - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(alice); world.revoke_writer(foo_selector, bob); } #[test] #[should_panic( - expected: ( - "Contract `foo_setter` does NOT have WRITER role on model (or its namespace) `Foo`", - 'ENTRYPOINT_FAILED', - 'ENTRYPOINT_FAILED' - ) + expected: "Contract `foo_setter` does NOT have WRITER role on model (or its namespace) `Foo`" )] fn test_not_writer_with_known_contract() { let (world, _) = deploy_world_and_foo(); let world = world.dispatcher; - let account = starknet::contract_address_const::<0xb0b>(); - world.grant_owner(bytearray_hash(@"dojo"), account); - - // the account owns the 'test_contract' namespace so it should be able to deploy - // and register the model. - starknet::testing::set_account_contract_address(account); - starknet::testing::set_contract_address(account); - let contract_address = world - .register_contract('salt1', "dojo", foo_setter::TEST_CLASS_HASH.try_into().unwrap()); + .register_contract('salt1', "dojo", snf_utils::declare_contract("foo_setter")); let d = IFooSetterDispatcher { contract_address }; d.set_foo(1, 2); - - core::panics::panic_with_byte_array( - @"Contract `dojo-foo_setter` does NOT have WRITER role on model (or its namespace) `Foo`" - ); } /// Test that an attacker can't control the hashes of resources in other namespaces /// by registering a model in an other namespace. #[test] #[should_panic( - expected: ( - "Account `7022365680606078322` does NOT have OWNER role on namespace `dojo`", - 'ENTRYPOINT_FAILED', - ) + expected: "Account `7022365680606078322` does NOT have OWNER role on namespace `dojo`" )] fn test_register_model_namespace_not_owner() { let owner = starknet::contract_address_const::<'owner'>(); let attacker = starknet::contract_address_const::<'attacker'>(); - starknet::testing::set_account_contract_address(owner); - starknet::testing::set_contract_address(owner); + snf_utils::set_account_address(owner); + snf_utils::set_caller_address(owner); // Owner deploys the world and register Foo model. let (world, foo_selector) = deploy_world_and_foo(); @@ -277,31 +251,28 @@ fn test_register_model_namespace_not_owner() { assert(world.is_owner(foo_selector, owner), 'should be owner'); - starknet::testing::set_contract_address(attacker); - starknet::testing::set_account_contract_address(attacker); + snf_utils::set_caller_address(attacker); + snf_utils::set_account_address(attacker); // Attacker has control over the this namespace. world.register_namespace("atk"); // Attacker can't take ownership of the Foo model in the dojo namespace. - world.register_model("dojo", attacker_model::TEST_CLASS_HASH.try_into().unwrap()); + world.register_model("dojo", snf_utils::declare_contract("attacker_model")); } /// Test that an attacker can't control the hashes of resources in other namespaces /// by deploying a contract in an other namespace. #[test] #[should_panic( - expected: ( - "Account `7022365680606078322` does NOT have OWNER role on namespace `dojo`", - 'ENTRYPOINT_FAILED', - ) + expected: "Account `7022365680606078322` does NOT have OWNER role on namespace `dojo`" )] fn test_register_contract_namespace_not_owner() { let owner = starknet::contract_address_const::<'owner'>(); let attacker = starknet::contract_address_const::<'attacker'>(); - starknet::testing::set_account_contract_address(owner); - starknet::testing::set_contract_address(owner); + snf_utils::set_account_address(owner); + snf_utils::set_caller_address(owner); // Owner deploys the world and register Foo model. let (world, foo_selector) = deploy_world_and_foo(); @@ -309,13 +280,12 @@ fn test_register_contract_namespace_not_owner() { assert(world.is_owner(foo_selector, owner), 'should be owner'); - starknet::testing::set_contract_address(attacker); - starknet::testing::set_account_contract_address(attacker); + snf_utils::set_caller_address(attacker); + snf_utils::set_account_address(attacker); // Attacker has control over the this namespace. world.register_namespace("atk"); // Attacker can't take ownership of the Foo model. - world - .register_contract('salt1', "dojo", attacker_contract::TEST_CLASS_HASH.try_into().unwrap()); + world.register_contract('salt1', "dojo", snf_utils::declare_contract("attacker_contract")); } diff --git a/crates/dojo/core-cairo-test/src/tests/world/contract.cairo b/crates/dojo/core-foundry-test/src/tests/world/contract.cairo similarity index 56% rename from crates/dojo/core-cairo-test/src/tests/world/contract.cairo rename to crates/dojo/core-foundry-test/src/tests/world/contract.cairo index 093427cac9..4ff6d5fbca 100644 --- a/crates/dojo/core-cairo-test/src/tests/world/contract.cairo +++ b/crates/dojo/core-foundry-test/src/tests/world/contract.cairo @@ -1,8 +1,10 @@ -use core::starknet::{ContractAddress, ClassHash}; use dojo::world::{world, IWorldDispatcherTrait}; use dojo::contract::components::upgradeable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; use dojo::meta::{IDeployedResourceDispatcher, IDeployedResourceDispatcherTrait}; -use crate::tests::helpers::{DOJO_NSH, test_contract, drop_all_events, deploy_world}; +use crate::tests::helpers::{DOJO_NSH, deploy_world}; +use crate::snf_utils; + +use snforge_std::{spy_events, EventSpyAssertionsTrait}; #[starknet::contract] pub mod contract_invalid_upgrade { @@ -64,10 +66,9 @@ fn test_upgrade_from_world() { let world = world.dispatcher; let base_address = world - .register_contract('salt', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); - let new_class_hash: ClassHash = test_contract_upgrade::TEST_CLASS_HASH.try_into().unwrap(); + .register_contract('salt', "dojo", snf_utils::declare_contract("test_contract")); - world.upgrade_contract("dojo", new_class_hash); + world.upgrade_contract("dojo", snf_utils::declare_contract("test_contract_upgrade")); let quantum_dispatcher = IQuantumLeapDispatcher { contract_address: base_address }; assert(quantum_dispatcher.plz_more_tps() == 'daddy', 'quantum leap failed'); @@ -80,26 +81,22 @@ fn test_upgrade_from_world_not_world_provider() { let world = deploy_world(); let world = world.dispatcher; - let _ = world - .register_contract('salt', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); - let new_class_hash: ClassHash = contract_invalid_upgrade::TEST_CLASS_HASH.try_into().unwrap(); - - world.upgrade_contract("dojo", new_class_hash); + let _ = world.register_contract('salt', "dojo", snf_utils::declare_contract("test_contract")); + world.upgrade_contract("dojo", snf_utils::declare_contract("contract_invalid_upgrade")); } #[test] #[available_gas(6000000)] -#[should_panic(expected: ('must be called by world', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: 'must be called by world')] fn test_upgrade_direct() { let world = deploy_world(); let world = world.dispatcher; let base_address = world - .register_contract('salt', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); - let new_class_hash: ClassHash = test_contract_upgrade::TEST_CLASS_HASH.try_into().unwrap(); + .register_contract('salt', "dojo", snf_utils::declare_contract("test_contract")); let upgradeable_dispatcher = IUpgradeableDispatcher { contract_address: base_address }; - upgradeable_dispatcher.upgrade(new_class_hash); + upgradeable_dispatcher.upgrade(snf_utils::declare_contract("test_contract_upgrade")); } #[starknet::interface] @@ -164,40 +161,40 @@ fn test_deploy_contract_for_namespace_owner() { let world = deploy_world(); let world = world.dispatcher; - let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); + let class_hash = snf_utils::declare_contract("test_contract"); let bob = starknet::contract_address_const::<0xb0b>(); world.grant_owner(DOJO_NSH, bob); // the account owns the 'test_contract' namespace so it should be able to deploy the contract. - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); - drop_all_events(world.contract_address); + let mut spy = spy_events(); let contract_address = world.register_contract('salt1', "dojo", class_hash); - let event = match starknet::testing::pop_log::(world.contract_address).unwrap() { - world::Event::ContractRegistered(event) => event, - _ => panic!("no ContractRegistered event"), - }; - - let contract = IDeployedResourceDispatcher { contract_address }; - let contract_name = contract.dojo_name(); - - assert(event.name == contract_name, 'bad name'); - assert(event.namespace == "dojo", 'bad namespace'); - assert(event.salt == 'salt1', 'bad event salt'); - assert(event.class_hash == class_hash, 'bad class_hash'); - assert( - event.address != core::num::traits::Zero::::zero(), 'bad contract address' - ); + spy + .assert_emitted( + @array![ + ( + world.contract_address, + world::Event::ContractRegistered( + world::ContractRegistered { + name: "test_contract", + namespace: "dojo", + address: contract_address, + class_hash: class_hash, + salt: 'salt1' + } + ) + ) + ] + ); } #[test] -#[should_panic( - expected: ("Account `2827` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED',) -)] +#[should_panic(expected: "Account `2827` does NOT have OWNER role on namespace `dojo`")] fn test_deploy_contract_for_namespace_writer() { let world = deploy_world(); let world = world.dispatcher; @@ -207,117 +204,109 @@ fn test_deploy_contract_for_namespace_writer() { // the account has write access to the 'test_contract' namespace so it should be able to deploy // the contract. - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); - world.register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); + world.register_contract('salt1', "dojo", snf_utils::declare_contract("test_contract")); } #[test] -#[should_panic( - expected: ("Account `2827` does NOT have OWNER role on namespace `dojo`", 'ENTRYPOINT_FAILED',) -)] +#[should_panic(expected: "Account `2827` does NOT have OWNER role on namespace `dojo`")] fn test_deploy_contract_no_namespace_owner_access() { let world = deploy_world(); let world = world.dispatcher; let bob = starknet::contract_address_const::<0xb0b>(); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); - world.register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); + world.register_contract('salt1', "dojo", snf_utils::declare_contract("test_contract")); } #[test] -#[should_panic(expected: ("Namespace `buzz_namespace` is not registered", 'ENTRYPOINT_FAILED',))] +#[should_panic(expected: "Namespace `buzz_namespace` is not registered")] fn test_deploy_contract_with_unregistered_namespace() { let world = deploy_world(); let world = world.dispatcher; world - .register_contract( - 'salt1', "buzz_namespace", test_contract::TEST_CLASS_HASH.try_into().unwrap() - ); + .register_contract('salt1', "buzz_namespace", snf_utils::declare_contract("test_contract")); } -// It's CONTRACT_NOT_DEPLOYED for now as in this example the contract is not a dojo contract +// It's ENTRYPOINT_NOT_FOUND for now as in this example the contract is not a dojo contract // and it's not the account that is calling the deploy_contract function. #[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] fn test_deploy_contract_through_malicious_contract() { let world = deploy_world(); let world = world.dispatcher; let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); + let malicious_contract = snf_utils::declare_and_deploy("malicious_contract"); world.grant_owner(DOJO_NSH, bob); // the account owns the 'test_contract' namespace so it should be able to deploy the contract. - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(malicious_contract); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(malicious_contract); - world.register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); + world.register_contract('salt1', "dojo", snf_utils::declare_contract("test_contract")); } #[test] fn test_upgrade_contract_from_resource_owner() { let world = deploy_world(); let world = world.dispatcher; - let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); + let class_hash = snf_utils::declare_contract("test_contract"); let bob = starknet::contract_address_const::<0xb0b>(); world.grant_owner(DOJO_NSH, bob); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); - let contract_address = world.register_contract('salt1', "dojo", class_hash); - let contract = IDeployedResourceDispatcher { contract_address }; - let contract_name = contract.dojo_name(); + let _ = world.register_contract('salt1', "dojo", class_hash); - drop_all_events(world.contract_address); + let mut spy = spy_events(); world.upgrade_contract("dojo", class_hash); - let event = starknet::testing::pop_log::(world.contract_address); - assert(event.is_some(), 'no event)'); - - if let world::Event::ContractUpgraded(event) = event.unwrap() { - assert( - event - .selector == dojo::utils::selector_from_namespace_and_name( - DOJO_NSH, @contract_name - ), - 'bad contract selector' + spy + .assert_emitted( + @array![ + ( + world.contract_address, + world::Event::ContractUpgraded( + world::ContractUpgraded { + selector: dojo::utils::selector_from_namespace_and_name( + DOJO_NSH, @"test_contract" + ), + class_hash: class_hash, + } + ) + ) + ] ); - assert(event.class_hash == class_hash, 'bad class_hash'); - } else { - core::panic_with_felt252('no ContractUpgraded event'); - }; } #[test] #[should_panic( - expected: ( - "Account `659918` does NOT have OWNER role on contract (or its namespace) `test_contract`", - 'ENTRYPOINT_FAILED', - ) + expected: "Account `659918` does NOT have OWNER role on contract (or its namespace) `test_contract`" )] fn test_upgrade_contract_from_resource_writer() { let world = deploy_world(); let world = world.dispatcher; - let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); + let class_hash = snf_utils::declare_contract("test_contract"); let bob = starknet::contract_address_const::<0xb0b>(); let alice = starknet::contract_address_const::<0xa11ce>(); world.grant_owner(DOJO_NSH, bob); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); let contract_address = world.register_contract('salt1', "dojo", class_hash); let contract = IDeployedResourceDispatcher { contract_address }; @@ -326,54 +315,51 @@ fn test_upgrade_contract_from_resource_writer() { world.grant_writer(contract_selector, alice); - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(alice); world.upgrade_contract("dojo", class_hash); } #[test] #[should_panic( - expected: ( - "Account `659918` does NOT have OWNER role on contract (or its namespace) `test_contract`", - 'ENTRYPOINT_FAILED', - ) + expected: "Account `659918` does NOT have OWNER role on contract (or its namespace) `test_contract`" )] fn test_upgrade_contract_from_random_account() { let world = deploy_world(); let world = world.dispatcher; - let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); + let class_hash = snf_utils::declare_contract("test_contract"); let _contract_address = world.register_contract('salt1', "dojo", class_hash); let alice = starknet::contract_address_const::<0xa11ce>(); - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(alice); world.upgrade_contract("dojo", class_hash); } #[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] fn test_upgrade_contract_through_malicious_contract() { let world = deploy_world(); let world = world.dispatcher; - let class_hash = test_contract::TEST_CLASS_HASH.try_into().unwrap(); + let class_hash = snf_utils::declare_contract("test_contract"); let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); + let malicious_contract = snf_utils::declare_and_deploy("malicious_contract"); world.grant_owner(DOJO_NSH, bob); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); let _contract_address = world.register_contract('salt1', "dojo", class_hash); - starknet::testing::set_contract_address(malicious_contract); + snf_utils::set_caller_address(malicious_contract); world.upgrade_contract("dojo", class_hash); } diff --git a/crates/dojo/core-foundry-test/src/tests/world/event.cairo b/crates/dojo/core-foundry-test/src/tests/world/event.cairo new file mode 100644 index 0000000000..4c4ac84ba2 --- /dev/null +++ b/crates/dojo/core-foundry-test/src/tests/world/event.cairo @@ -0,0 +1,260 @@ +use core::starknet::ContractAddress; + +use crate::tests::helpers::{DOJO_NSH, deploy_world, deploy_world_for_event_upgrades}; +use dojo::world::IWorldDispatcherTrait; +use dojo::event::Event; +use crate::snf_utils; + +use snforge_std::{spy_events, EventSpyTrait, EventsFilterTrait}; + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::event] +pub struct FooEventBadLayoutType { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::event] +pub struct FooEventMemberRemoved { + #[key] + pub caller: ContractAddress, + pub b: u128, +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::event] +pub struct FooEventMemberAddedButRemoved { + #[key] + pub caller: ContractAddress, + pub b: u128, + pub c: u256, + pub d: u256 +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::event] +pub struct FooEventMemberAddedButMoved { + #[key] + pub caller: ContractAddress, + pub b: u128, + pub a: felt252, + pub c: u256 +} + +#[derive(Copy, Drop, Serde, Debug)] +#[dojo::event] +pub struct FooEventMemberAdded { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, + pub c: u256 +} + +#[test] +fn test_register_event_for_namespace_owner() { + let bob = starknet::contract_address_const::<0xb0b>(); + + let world = deploy_world(); + let world = world.dispatcher; + + world.grant_owner(DOJO_NSH, bob); + + let mut spy = spy_events(); + + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); + + let class_hash = snf_utils::declare_event_contract("SimpleEvent"); + world.register_event("dojo", class_hash); + + // parse the event manually because we don't know the value of + // the 'address' field of the emitted event to assert a full event. + let events = spy.get_events().emitted_by(world.contract_address); + + assert(events.events.len() == 1, 'There should be one event'); + + let (_, event) = events.events.at(0); + let mut keys = event.keys.span(); + + let event_name = *keys.pop_front().unwrap(); + let name: ByteArray = core::serde::Serde::deserialize(ref keys).unwrap(); + let ns: ByteArray = core::serde::Serde::deserialize(ref keys).unwrap(); + + assert(event_name == selector!("EventRegistered"), 'Wrong event name'); + assert(name == "SimpleEvent", 'Wrong name'); + assert(ns == "dojo", 'Wrong namespace'); + assert(event.data.at(0) == @class_hash.into(), 'Wrong class hash'); +} + +#[test] +#[should_panic(expected: "Account `2827` does NOT have OWNER role on namespace `dojo`")] +fn test_register_event_for_namespace_writer() { + let bob = starknet::contract_address_const::<0xb0b>(); + + let world = deploy_world(); + let world = world.dispatcher; + + world.grant_writer(DOJO_NSH, bob); + + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); + world.register_event("dojo", snf_utils::declare_event_contract("SimpleEvent")); +} + +#[test] +fn test_upgrade_event_from_event_owner() { + let bob = starknet::contract_address_const::<0xb0b>(); + + let world = deploy_world_for_event_upgrades(); + world.grant_owner(Event::::selector(DOJO_NSH), bob); + + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); + + let mut spy = spy_events(); + + let class_hash = snf_utils::declare_event_contract("FooEventMemberAdded"); + world.upgrade_event("dojo", class_hash); + + // parse the event manually because we don't know the value of + // the 'address' field of the emitted event to assert a full event. + let events = spy.get_events().emitted_by(world.contract_address); + + assert(events.events.len() == 1, 'There should be one event'); + + let (_, event) = events.events.at(0); + + assert(event.keys.at(0) == @selector!("EventUpgraded"), 'Wrong event name'); + assert( + event.keys.at(1) == @Event::::selector(DOJO_NSH), 'bad model selector' + ); + assert(event.data.at(0) == @class_hash.into(), 'Wrong class hash'); + + assert( + world.is_owner(Event::::selector(DOJO_NSH), bob), + 'bob is not the owner' + ); +} + +#[test] +fn test_upgrade_event() { + let world = deploy_world_for_event_upgrades(); + + let mut spy = spy_events(); + + let class_hash = snf_utils::declare_event_contract("FooEventMemberAdded"); + world.upgrade_event("dojo", class_hash); + + // parse the event manually because we don't know the value of + // the 'address' field of the emitted event to assert a full event. + let events = spy.get_events().emitted_by(world.contract_address); + + assert(events.events.len() == 1, 'There should be one event'); + + let (_, event) = events.events.at(0); + + assert(event.keys.at(0) == @selector!("EventUpgraded"), 'Wrong event name'); + assert( + event.keys.at(1) == @Event::::selector(DOJO_NSH), 'bad model selector' + ); + assert(event.data.at(0) == @class_hash.into(), 'Wrong class hash'); +} + +#[test] +#[should_panic(expected: "Invalid new layout to upgrade the resource `dojo-FooEventBadLayoutType`")] +fn test_upgrade_event_with_bad_layout_type() { + let world = deploy_world_for_event_upgrades(); + world.upgrade_event("dojo", snf_utils::declare_event_contract("FooEventBadLayoutType")); +} + +#[test] +#[should_panic(expected: "Invalid new schema to upgrade the resource `dojo-FooEventMemberRemoved`")] +fn test_upgrade_event_with_member_removed() { + let world = deploy_world_for_event_upgrades(); + world.upgrade_event("dojo", snf_utils::declare_event_contract("FooEventMemberRemoved")); +} + +#[test] +#[should_panic( + expected: "Invalid new schema to upgrade the resource `dojo-FooEventMemberAddedButRemoved`" +)] +fn test_upgrade_event_with_member_added_but_removed() { + let world = deploy_world_for_event_upgrades(); + world.upgrade_event("dojo", snf_utils::declare_event_contract("FooEventMemberAddedButRemoved")); +} + +#[test] +#[should_panic( + expected: "Invalid new schema to upgrade the resource `dojo-FooEventMemberAddedButMoved`" +)] +fn test_upgrade_event_with_member_moved() { + let world = deploy_world_for_event_upgrades(); + world.upgrade_event("dojo", snf_utils::declare_event_contract("FooEventMemberAddedButMoved")); +} + +#[test] +#[should_panic( + expected: "Account `659918` does NOT have OWNER role on event (or its namespace) `FooEventMemberAdded`" +)] +fn test_upgrade_event_from_event_writer() { + let alice = starknet::contract_address_const::<0xa11ce>(); + + let world = deploy_world_for_event_upgrades(); + + world.grant_writer(Event::::selector(DOJO_NSH), alice); + + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(alice); + world.upgrade_event("dojo", snf_utils::declare_event_contract("FooEventMemberAdded")); +} + +#[test] +#[should_panic(expected: "Resource `dojo-SimpleEvent` is already registered")] +fn test_upgrade_event_from_random_account() { + let bob = starknet::contract_address_const::<0xb0b>(); + let alice = starknet::contract_address_const::<0xa11ce>(); + + let world = deploy_world(); + let world = world.dispatcher; + + world.grant_owner(DOJO_NSH, bob); + world.grant_owner(DOJO_NSH, alice); + + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); + world.register_event("dojo", snf_utils::declare_event_contract("SimpleEvent")); + + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(alice); + world.register_event("dojo", snf_utils::declare_event_contract("SimpleEvent")); +} + +#[test] +#[should_panic(expected: "Namespace `another_namespace` is not registered")] +fn test_register_event_with_unregistered_namespace() { + let world = deploy_world(); + let world = world.dispatcher; + + world.register_event("another_namespace", snf_utils::declare_event_contract("SimpleEvent")); +} + +// It's ENTRYPOINT_NOT_FOUND for now as in this example the contract is not a dojo contract +// and it's not the account that is calling the register_event function. +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] +fn test_register_event_through_malicious_contract() { + let bob = starknet::contract_address_const::<0xb0b>(); + let malicious_contract = snf_utils::declare_and_deploy("malicious_contract"); + + let world = deploy_world(); + let world = world.dispatcher; + world.grant_owner(DOJO_NSH, bob); + + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(malicious_contract); + world.register_event("dojo", snf_utils::declare_event_contract("SimpleEvent")); +} diff --git a/crates/dojo/core-cairo-test/src/tests/world/metadata.cairo b/crates/dojo/core-foundry-test/src/tests/world/metadata.cairo similarity index 59% rename from crates/dojo/core-cairo-test/src/tests/world/metadata.cairo rename to crates/dojo/core-foundry-test/src/tests/world/metadata.cairo index 0471b82d62..05510b2caa 100644 --- a/crates/dojo/core-cairo-test/src/tests/world/metadata.cairo +++ b/crates/dojo/core-foundry-test/src/tests/world/metadata.cairo @@ -1,10 +1,20 @@ use dojo::world::{world, IWorldDispatcherTrait}; use dojo::model::{Model, ResourceMetadata}; -use crate::tests::helpers::{DOJO_NSH, Foo, drop_all_events, deploy_world, deploy_world_and_foo}; +use crate::tests::helpers::{DOJO_NSH, Foo, deploy_world, deploy_world_and_foo}; +use crate::snf_utils; + +use snforge_std::{spy_events, EventSpyAssertionsTrait}; #[test] fn test_set_metadata_world() { + // deploy a dedicated contract to be used as caller/account address because of + // the way `world.panic_with_details()` is written. + // Once this function will use SRC5, we will be able to remove these lines + let caller_contract = snf_utils::declare_and_deploy("dojo_caller_contract"); + snf_utils::set_caller_address(caller_contract); + snf_utils::set_account_address(caller_contract); + let world = deploy_world(); let world = world.dispatcher; @@ -26,38 +36,38 @@ fn test_set_metadata_resource_owner() { world.grant_owner(Model::::selector(DOJO_NSH), bob); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); let metadata = ResourceMetadata { resource_id: model_selector, metadata_uri: format!("ipfs:bob"), metadata_hash: 42 }; - drop_all_events(world.contract_address); + let mut spy = spy_events(); // Metadata must be updated by a direct call from an account which has owner role // for the attached resource. world.set_metadata(metadata.clone()); assert(world.metadata(model_selector) == metadata, 'bad metadata'); - let event = starknet::testing::pop_log::(world.contract_address); - assert(event.is_some(), 'no event)'); - - if let world::Event::MetadataUpdate(event) = event.unwrap() { - assert(event.resource == metadata.resource_id, 'bad resource'); - assert(event.uri == metadata.metadata_uri, 'bad uri'); - assert(event.hash == metadata.metadata_hash, 'bad hash'); - } else { - core::panic_with_felt252('no EventUpgraded event'); - } + spy + .assert_emitted( + @array![ + ( + world.contract_address, + world::Event::MetadataUpdate( + world::MetadataUpdate { + resource: metadata.resource_id, uri: metadata.metadata_uri, hash: metadata.metadata_hash + } + ) + ) + ] + ); } #[test] #[should_panic( - expected: ( - "Account `2827` does NOT have OWNER role on model (or its namespace) `Foo`", - 'ENTRYPOINT_FAILED', - ) + expected: "Account `2827` does NOT have OWNER role on model (or its namespace) `Foo`" )] fn test_set_metadata_not_possible_for_resource_writer() { let (world, model_selector) = deploy_world_and_foo(); @@ -67,8 +77,8 @@ fn test_set_metadata_not_possible_for_resource_writer() { world.grant_writer(model_selector, bob); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); let metadata = ResourceMetadata { resource_id: model_selector, metadata_uri: format!("ipfs:bob"), metadata_hash: 42 @@ -78,9 +88,7 @@ fn test_set_metadata_not_possible_for_resource_writer() { } #[test] -#[should_panic( - expected: ("Account `2827` does NOT have OWNER role on world", 'ENTRYPOINT_FAILED',) -)] +#[should_panic(expected: "Account `2827` does NOT have OWNER role on world")] fn test_set_metadata_not_possible_for_random_account() { let world = deploy_world(); let world = world.dispatcher; @@ -90,8 +98,8 @@ fn test_set_metadata_not_possible_for_random_account() { }; let bob = starknet::contract_address_const::<0xb0b>(); - starknet::testing::set_contract_address(bob); - starknet::testing::set_account_contract_address(bob); + snf_utils::set_caller_address(bob); + snf_utils::set_account_address(bob); // Bob access follows the conventional ACL, he can't write the world // metadata if he does not have access to it. @@ -99,18 +107,18 @@ fn test_set_metadata_not_possible_for_random_account() { } #[test] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED',))] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] fn test_set_metadata_through_malicious_contract() { let (world, model_selector) = deploy_world_and_foo(); let world = world.dispatcher; let bob = starknet::contract_address_const::<0xb0b>(); - let malicious_contract = starknet::contract_address_const::<0xdead>(); + let malicious_contract = snf_utils::declare_and_deploy("malicious_contract"); world.grant_owner(model_selector, bob); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(malicious_contract); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(malicious_contract); let metadata = ResourceMetadata { resource_id: model_selector, metadata_uri: format!("ipfs:bob"), metadata_hash: 42 diff --git a/crates/dojo/core-foundry-test/src/tests/world/model.cairo b/crates/dojo/core-foundry-test/src/tests/world/model.cairo new file mode 100644 index 0000000000..89b7a5a31d --- /dev/null +++ b/crates/dojo/core-foundry-test/src/tests/world/model.cairo @@ -0,0 +1,271 @@ +use core::starknet::ContractAddress; + +use crate::tests::helpers::{Foo, DOJO_NSH, deploy_world, deploy_world_for_model_upgrades}; +use dojo::world::IWorldDispatcherTrait; +use dojo::model::Model; +use crate::snf_utils; + +use snforge_std::{spy_events, EventSpyTrait, EventsFilterTrait}; + +#[derive(Copy, Drop, Serde)] +#[dojo::model] +pub struct FooModelBadLayoutType { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, +} + +#[derive(Copy, Drop, Serde)] +#[dojo::model] +pub struct FooModelMemberRemoved { + #[key] + pub caller: ContractAddress, + pub b: u128, +} + +#[derive(Copy, Drop, Serde)] +#[dojo::model] +pub struct FooModelMemberAddedButRemoved { + #[key] + pub caller: ContractAddress, + pub b: u128, + pub c: u256, + pub d: u256 +} + +#[derive(Copy, Drop, Serde)] +#[dojo::model] +pub struct FooModelMemberAddedButMoved { + #[key] + pub caller: ContractAddress, + pub b: u128, + pub a: felt252, + pub c: u256 +} + +#[derive(Copy, Drop, Serde)] +#[dojo::model] +pub struct FooModelMemberAdded { + #[key] + pub caller: ContractAddress, + pub a: felt252, + pub b: u128, + pub c: u256 +} + +#[test] +fn test_register_model_for_namespace_owner() { + let bob = starknet::contract_address_const::<0xb0b>(); + + let world = deploy_world(); + let world = world.dispatcher; + + world.grant_owner(DOJO_NSH, bob); + + let mut spy = spy_events(); + + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); + + let class_hash = snf_utils::declare_model_contract("Foo"); + world.register_model("dojo", class_hash); + + // parse the event manually because we don't know the value of + // the 'address' field of the emitted event to assert a full event. + let events = spy.get_events().emitted_by(world.contract_address); + + assert(events.events.len() == 1, 'There should be one event'); + + let (_, event) = events.events.at(0); + let mut keys = event.keys.span(); + + let event_name = *keys.pop_front().unwrap(); + let name: ByteArray = core::serde::Serde::deserialize(ref keys).unwrap(); + let ns: ByteArray = core::serde::Serde::deserialize(ref keys).unwrap(); + + assert(event_name == selector!("ModelRegistered"), 'Wrong event name'); + assert(name == "Foo", 'Wrong name'); + assert(ns == "dojo", 'Wrong namespace'); + assert(event.data.at(0) == @class_hash.into(), 'Wrong class hash'); + + assert(world.is_owner(Model::::selector(DOJO_NSH), bob), 'bob is not the owner'); +} + +#[test] +#[should_panic( + expected: "Name `foo-bis` is invalid according to Dojo naming rules: ^[a-zA-Z0-9_]+$" +)] +fn test_register_model_with_invalid_name() { + let world = deploy_world(); + let world = world.dispatcher; + + world.register_model("dojo", snf_utils::declare_model_contract("FooInvalidName")); +} + +#[test] +#[should_panic(expected: "Account `2827` does NOT have OWNER role on namespace `dojo`")] +fn test_register_model_for_namespace_writer() { + let bob = starknet::contract_address_const::<0xb0b>(); + + let world = deploy_world(); + let world = world.dispatcher; + + world.grant_writer(DOJO_NSH, bob); + + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); + world.register_model("dojo", snf_utils::declare_model_contract("Foo")); +} + +#[test] +fn test_upgrade_model_from_model_owner() { + let bob = starknet::contract_address_const::<0xb0b>(); + + let world = deploy_world_for_model_upgrades(); + + world.grant_owner(Model::::selector(DOJO_NSH), bob); + + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); + + let mut spy = spy_events(); + + let class_hash = snf_utils::declare_model_contract("FooModelMemberAdded"); + world.upgrade_model("dojo", class_hash); + + // parse the event manually because we don't know the value of + // the 'address' field of the emitted event to assert a full event. + let events = spy.get_events().emitted_by(world.contract_address); + + assert(events.events.len() == 1, 'There should be one event'); + + let (_, event) = events.events.at(0); + + assert(event.keys.at(0) == @selector!("ModelUpgraded"), 'Wrong event name'); + assert(event.keys.at(1) == @Model::::selector(DOJO_NSH), 'Wrong selector'); + assert(event.data.at(0) == @class_hash.into(), 'Wrong class hash'); + + assert( + world.is_owner(Model::::selector(DOJO_NSH), bob), + 'bob is not the owner' + ); +} + +#[test] +fn test_upgrade_model() { + let world = deploy_world_for_model_upgrades(); + + let mut spy = spy_events(); + + let class_hash = snf_utils::declare_model_contract("FooModelMemberAdded"); + world.upgrade_model("dojo", class_hash); + + // parse the event manually because we don't know the value of + // the 'address' field of the emitted event to assert a full event. + let events = spy.get_events().emitted_by(world.contract_address); + + assert(events.events.len() == 1, 'There should be one event'); + + let (_, event) = events.events.at(0); + + assert(event.keys.at(0) == @selector!("ModelUpgraded"), 'Wrong event name'); + assert(event.keys.at(1) == @Model::::selector(DOJO_NSH), 'Wrong selector'); + assert(event.data.at(0) == @class_hash.into(), 'Wrong class hash'); +} + +#[test] +#[should_panic(expected: "Invalid new layout to upgrade the resource `dojo-FooModelBadLayoutType`")] +fn test_upgrade_model_with_bad_layout_type() { + let world = deploy_world_for_model_upgrades(); + world.upgrade_model("dojo", snf_utils::declare_model_contract("FooModelBadLayoutType")); +} + +#[test] +#[should_panic(expected: "Invalid new schema to upgrade the resource `dojo-FooModelMemberRemoved`")] +fn test_upgrade_model_with_member_removed() { + let world = deploy_world_for_model_upgrades(); + world.upgrade_model("dojo", snf_utils::declare_model_contract("FooModelMemberRemoved")); +} + +#[test] +#[should_panic( + expected: "Invalid new schema to upgrade the resource `dojo-FooModelMemberAddedButRemoved`" +)] +fn test_upgrade_model_with_member_added_but_removed() { + let world = deploy_world_for_model_upgrades(); + world.upgrade_model("dojo", snf_utils::declare_model_contract("FooModelMemberAddedButRemoved")); +} + +#[test] +#[should_panic( + expected: "Invalid new schema to upgrade the resource `dojo-FooModelMemberAddedButMoved`" +)] +fn test_upgrade_model_with_member_moved() { + let world = deploy_world_for_model_upgrades(); + world.upgrade_model("dojo", snf_utils::declare_model_contract("FooModelMemberAddedButMoved")); +} + +#[test] +#[should_panic( + expected: "Account `659918` does NOT have OWNER role on model (or its namespace) `FooModelMemberAdded`" +)] +fn test_upgrade_model_from_model_writer() { + let alice = starknet::contract_address_const::<0xa11ce>(); + + let world = deploy_world_for_model_upgrades(); + + world.grant_writer(Model::::selector(DOJO_NSH), alice); + + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(alice); + world.upgrade_model("dojo", snf_utils::declare_model_contract("FooModelMemberAdded")); +} + +#[test] +#[should_panic(expected: "Resource `dojo-Foo` is already registered")] +fn test_upgrade_model_from_random_account() { + let bob = starknet::contract_address_const::<0xb0b>(); + let alice = starknet::contract_address_const::<0xa11ce>(); + + let world = deploy_world(); + let world = world.dispatcher; + + world.grant_owner(DOJO_NSH, bob); + world.grant_owner(DOJO_NSH, alice); + + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); + world.register_model("dojo", snf_utils::declare_model_contract("Foo")); + + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(alice); + world.register_model("dojo", snf_utils::declare_model_contract("Foo")); +} + +#[test] +#[should_panic(expected: "Namespace `another_namespace` is not registered")] +fn test_register_model_with_unregistered_namespace() { + let world = deploy_world(); + let world = world.dispatcher; + + world.register_model("another_namespace", snf_utils::declare_model_contract("Foo")); +} + +// It's ENTRYPOINT_NOT_FOUND for now as in this example the contract is not a dojo contract +// and it's not the account that is calling the register_model function. +#[test] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] +fn test_register_model_through_malicious_contract() { + let bob = starknet::contract_address_const::<0xb0b>(); + let malicious_contract = snf_utils::declare_and_deploy("malicious_contract"); + + let world = deploy_world(); + let world = world.dispatcher; + + world.grant_owner(DOJO_NSH, bob); + + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(malicious_contract); + world.register_model("dojo", snf_utils::declare_model_contract("Foo")); +} diff --git a/crates/dojo/core-cairo-test/src/tests/world/namespace.cairo b/crates/dojo/core-foundry-test/src/tests/world/namespace.cairo similarity index 50% rename from crates/dojo/core-cairo-test/src/tests/world/namespace.cairo rename to crates/dojo/core-foundry-test/src/tests/world/namespace.cairo index 12883604fa..136fcc6255 100644 --- a/crates/dojo/core-cairo-test/src/tests/world/namespace.cairo +++ b/crates/dojo/core-foundry-test/src/tests/world/namespace.cairo @@ -1,7 +1,10 @@ use dojo::world::{world, IWorldDispatcherTrait}; use dojo::utils::bytearray_hash; -use crate::tests::helpers::{drop_all_events, deploy_world}; +use crate::tests::helpers::deploy_world; +use crate::snf_utils; + +use snforge_std::{spy_events, EventSpyAssertionsTrait}; #[test] fn test_register_namespace() { @@ -9,10 +12,10 @@ fn test_register_namespace() { let world = world.dispatcher; let bob = starknet::contract_address_const::<0xb0b>(); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); - drop_all_events(world.contract_address); + let mut spy = spy_events(); let namespace = "namespace"; let hash = bytearray_hash(@namespace); @@ -21,44 +24,48 @@ fn test_register_namespace() { assert(world.is_owner(hash, bob), 'namespace not registered'); - match starknet::testing::pop_log::(world.contract_address).unwrap() { - world::Event::NamespaceRegistered(event) => { - assert(event.namespace == namespace, 'bad namespace'); - assert(event.hash == hash, 'bad hash'); - }, - _ => panic!("no NamespaceRegistered event"), - } + spy + .assert_emitted( + @array![ + ( + world.contract_address, + world::Event::NamespaceRegistered( + world::NamespaceRegistered { namespace, hash } + ) + ) + ] + ); } #[test] -#[should_panic(expected: ("Namespace `namespace` is already registered", 'ENTRYPOINT_FAILED',))] +#[should_panic(expected: "Namespace `namespace` is already registered")] fn test_register_namespace_already_registered_same_caller() { let world = deploy_world(); let world = world.dispatcher; let bob = starknet::contract_address_const::<0xb0b>(); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); world.register_namespace("namespace"); world.register_namespace("namespace"); } #[test] -#[should_panic(expected: ("Namespace `namespace` is already registered", 'ENTRYPOINT_FAILED',))] +#[should_panic(expected: "Namespace `namespace` is already registered")] fn test_register_namespace_already_registered_other_caller() { let world = deploy_world(); let world = world.dispatcher; let bob = starknet::contract_address_const::<0xb0b>(); - starknet::testing::set_account_contract_address(bob); - starknet::testing::set_contract_address(bob); + snf_utils::set_account_address(bob); + snf_utils::set_caller_address(bob); world.register_namespace("namespace"); let alice = starknet::contract_address_const::<0xa11ce>(); - starknet::testing::set_account_contract_address(alice); - starknet::testing::set_contract_address(alice); + snf_utils::set_account_address(alice); + snf_utils::set_caller_address(alice); world.register_namespace("namespace"); } @@ -66,12 +73,7 @@ fn test_register_namespace_already_registered_other_caller() { #[test] #[available_gas(6000000)] -#[should_panic( - expected: ( - "Namespace `` is invalid according to Dojo naming rules: ^[a-zA-Z0-9_]+$", - 'ENTRYPOINT_FAILED', - ) -)] +#[should_panic(expected: "Namespace `` is invalid according to Dojo naming rules: ^[a-zA-Z0-9_]+$")] fn test_register_namespace_empty_name() { let world = deploy_world(); let world = world.dispatcher; diff --git a/crates/dojo/core-cairo-test/src/tests/world/storage.cairo b/crates/dojo/core-foundry-test/src/tests/world/storage.cairo similarity index 100% rename from crates/dojo/core-cairo-test/src/tests/world/storage.cairo rename to crates/dojo/core-foundry-test/src/tests/world/storage.cairo diff --git a/crates/dojo/core-cairo-test/src/tests/world/world.cairo b/crates/dojo/core-foundry-test/src/tests/world/world.cairo similarity index 67% rename from crates/dojo/core-cairo-test/src/tests/world/world.cairo rename to crates/dojo/core-foundry-test/src/tests/world/world.cairo index 47b6818a50..a692fc74d5 100644 --- a/crates/dojo/core-cairo-test/src/tests/world/world.cairo +++ b/crates/dojo/core-foundry-test/src/tests/world/world.cairo @@ -1,51 +1,57 @@ use dojo::world::Resource; -use dojo::world::world::Event as WorldEvent; use dojo::utils::bytearray_hash; use dojo::world::{ - IWorldDispatcher, IWorldDispatcherTrait, IUpgradeableWorldDispatcher, + world, IWorldDispatcher, IWorldDispatcherTrait, IUpgradeableWorldDispatcher, IUpgradeableWorldDispatcherTrait, WorldStorageTrait }; use dojo::model::ModelStorage; use dojo::event::{Event, EventStorage}; use crate::tests::helpers::{ - IbarDispatcherTrait, drop_all_events, deploy_world_and_bar, Foo, m_Foo, test_contract, - test_contract_with_dojo_init_args, SimpleEvent, e_SimpleEvent, deploy_world + IbarDispatcherTrait, deploy_world_and_bar, Foo, SimpleEvent, deploy_world }; use crate::{spawn_test_world, ContractDefTrait, NamespaceDef, TestResource, WorldStorageTestTrait}; +use crate::snf_utils; + +use snforge_std::{spy_events, EventSpyAssertionsTrait}; + #[test] #[available_gas(20000000)] fn test_model() { let world = deploy_world(); let world = world.dispatcher; - world.register_model("dojo", m_Foo::TEST_CLASS_HASH.try_into().unwrap()); + world.register_model("dojo", snf_utils::declare_model_contract("Foo")); } #[test] fn test_system() { + let caller = snforge_std::test_address(); + let (world, bar_contract) = deploy_world_and_bar(); bar_contract.set_foo(1337, 1337); - let stored: Foo = world.read_model(starknet::get_caller_address()); + let stored: Foo = world.read_model(caller); assert(stored.a == 1337, 'data not stored'); assert(stored.b == 1337, 'data not stored'); } #[test] fn test_delete() { + let caller = snforge_std::test_address(); + let (world, bar_contract) = deploy_world_and_bar(); bar_contract.set_foo(1337, 1337); - let stored: Foo = world.read_model(starknet::get_caller_address()); + let stored: Foo = world.read_model(caller); assert(stored.a == 1337, 'data not stored'); assert(stored.b == 1337, 'data not stored'); bar_contract.delete_foo(); - let deleted: Foo = world.read_model(starknet::get_caller_address()); + let deleted: Foo = world.read_model(caller); assert(deleted.a == 0, 'data not deleted'); assert(deleted.b == 0, 'data not deleted'); } @@ -57,7 +63,7 @@ fn test_contract_getter() { let world = world.dispatcher; let address = world - .register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); + .register_contract('salt1', "dojo", snf_utils::declare_contract("test_contract")); if let Resource::Contract((contract_address, namespace_hash)) = world .resource(selector_from_tag!("dojo-test_contract")) { @@ -73,7 +79,7 @@ fn test_emit() { let bob = starknet::contract_address_const::<0xb0b>(); let namespace_def = NamespaceDef { - namespace: "dojo", resources: [TestResource::Event(e_SimpleEvent::TEST_CLASS_HASH),].span(), + namespace: "dojo", resources: [TestResource::Event("SimpleEvent"),].span(), }; let mut world = spawn_test_world([namespace_def].span()); @@ -82,43 +88,46 @@ fn test_emit() { .with_writer_of([world.resource_selector(@"SimpleEvent")].span()); world.sync_perms_and_inits([bob_def].span()); - drop_all_events(world.dispatcher.contract_address); + let mut spy = spy_events(); - starknet::testing::set_contract_address(bob); + snf_utils::set_caller_address(bob); let simple_event = SimpleEvent { id: 2, data: (3, 4) }; world.emit_event(@simple_event); - let event = starknet::testing::pop_log::(world.dispatcher.contract_address); - - assert(event.is_some(), 'no event'); - - if let WorldEvent::EventEmitted(event) = event.unwrap() { - assert( - event.selector == Event::::selector(world.namespace_hash), - 'bad event selector' + spy + .assert_emitted( + @array![ + ( + world.dispatcher.contract_address, + world::Event::EventEmitted( + world::EventEmitted { + selector: Event::::selector(world.namespace_hash), + system_address: bob, + keys: [ + 2 + ].span(), values: [ + 3, 4 + ].span() + } + ) + ) + ] ); - assert(event.system_address == bob, 'bad system address'); - assert(event.keys == [2].span(), 'bad keys'); - assert(event.values == [3, 4].span(), 'bad values'); - } else { - core::panic_with_felt252('no EventEmitted event'); - } } #[test] fn test_execute_multiple_worlds() { + let caller = snforge_std::test_address(); + let (world1, bar1_contract) = deploy_world_and_bar(); let (world2, bar2_contract) = deploy_world_and_bar(); - 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 data1: Foo = world1.read_model(alice); - let data2: Foo = world2.read_model(alice); + let data1: Foo = world1.read_model(caller); + let data2: Foo = world2.read_model(caller); assert(data1.a == 1337, 'data1 not stored'); assert(data2.a == 7331, 'data2 not stored'); @@ -150,13 +159,17 @@ mod worldupgrade { #[test] #[available_gas(60000000)] fn test_upgradeable_world() { + let caller = snf_utils::declare_and_deploy("dojo_caller_contract"); + snf_utils::set_account_address(caller); + snf_utils::set_caller_address(caller); + let world = deploy_world(); let world = world.dispatcher; let mut upgradeable_world_dispatcher = IUpgradeableWorldDispatcher { contract_address: world.contract_address }; - upgradeable_world_dispatcher.upgrade(worldupgrade::TEST_CLASS_HASH.try_into().unwrap()); + upgradeable_world_dispatcher.upgrade(snf_utils::declare_contract("worldupgrade")); let res = (IWorldUpgradeDispatcher { contract_address: world.contract_address }).hello(); @@ -165,14 +178,14 @@ fn test_upgradeable_world() { #[test] #[available_gas(60000000)] -#[should_panic(expected: ('invalid class_hash', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: 'invalid class_hash')] fn test_upgradeable_world_with_class_hash_zero() { let world = deploy_world(); let world = world.dispatcher; let admin = starknet::contract_address_const::<0x1337>(); - starknet::testing::set_account_contract_address(admin); - starknet::testing::set_contract_address(admin); + snf_utils::set_account_address(admin); + snf_utils::set_caller_address(admin); let mut upgradeable_world_dispatcher = IUpgradeableWorldDispatcher { contract_address: world.contract_address @@ -182,17 +195,15 @@ fn test_upgradeable_world_with_class_hash_zero() { #[test] #[available_gas(60000000)] -#[should_panic( - expected: ("Caller `4919` cannot upgrade the resource `0` (not owner)", 'ENTRYPOINT_FAILED') -)] +#[should_panic(expected: "Caller `4919` cannot upgrade the resource `0` (not owner)")] fn test_upgradeable_world_from_non_owner() { // Deploy world contract let world = deploy_world(); let world = world.dispatcher; let not_owner = starknet::contract_address_const::<0x1337>(); - starknet::testing::set_contract_address(not_owner); - starknet::testing::set_account_contract_address(not_owner); + snf_utils::set_caller_address(not_owner); + snf_utils::set_account_address(not_owner); let mut upgradeable_world_dispatcher = IUpgradeableWorldDispatcher { contract_address: world.contract_address @@ -207,7 +218,7 @@ fn test_constructor_default() { let world = world.dispatcher; let _address = world - .register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); + .register_contract('salt1', "dojo", snf_utils::declare_contract("test_contract")); } #[test] @@ -216,10 +227,12 @@ fn test_can_call_init_only_world() { let world = world.dispatcher; let address = world - .register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); + .register_contract('salt1', "dojo", snf_utils::declare_contract("test_contract")); let expected_panic: ByteArray = - "Only the world can init contract `test_contract`, but caller is `0`"; + "Only the world can init contract `test_contract`, but caller is `2827`"; + + snf_utils::set_caller_address(starknet::contract_address_const::<2827>()); match starknet::syscalls::call_contract_syscall( address, dojo::world::world::DOJO_INIT_SELECTOR, [].span() @@ -229,8 +242,6 @@ fn test_can_call_init_only_world() { let mut s = e.span(); // Remove the out of range error. s.pop_front().unwrap(); - // Remove the ENTRYPOINT_FAILED suffix. - s.pop_back().unwrap(); let e_str: ByteArray = Serde::deserialize(ref s).expect('failed deser'); println!("e_str: {}", e_str); @@ -241,16 +252,16 @@ fn test_can_call_init_only_world() { #[test] #[available_gas(6000000)] -#[should_panic(expected: ('CONTRACT_NOT_DEPLOYED', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('ENTRYPOINT_NOT_FOUND', 'ENTRYPOINT_FAILED'))] fn test_can_call_init_only_owner() { let world = deploy_world(); let world = world.dispatcher; let _address = world - .register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); + .register_contract('salt1', "dojo", snf_utils::declare_contract("test_contract")); - let bob = starknet::contract_address_const::<0x1337>(); - starknet::testing::set_contract_address(bob); + let caller_contract = snf_utils::declare_and_deploy("non_dojo_caller_contract"); + snf_utils::set_caller_address(caller_contract); world.init_contract(selector_from_tag!("dojo-test_contract"), [].span()); } @@ -262,7 +273,7 @@ fn test_can_call_init_default() { let world = world.dispatcher; let _address = world - .register_contract('salt1', "dojo", test_contract::TEST_CLASS_HASH.try_into().unwrap()); + .register_contract('salt1', "dojo", snf_utils::declare_contract("test_contract")); world.init_contract(selector_from_tag!("dojo-test_contract"), [].span()); } @@ -275,7 +286,7 @@ fn test_can_call_init_args() { let _address = world .register_contract( - 'salt1', "dojo", test_contract_with_dojo_init_args::TEST_CLASS_HASH.try_into().unwrap() + 'salt1', "dojo", snf_utils::declare_contract("test_contract_with_dojo_init_args") ); world.init_contract(selector_from_tag!("dojo-test_contract_with_dojo_init_args"), [1].span()); @@ -288,11 +299,14 @@ fn test_can_call_init_only_world_args() { let address = world .register_contract( - 'salt1', "dojo", test_contract_with_dojo_init_args::TEST_CLASS_HASH.try_into().unwrap() + 'salt1', "dojo", snf_utils::declare_contract("test_contract_with_dojo_init_args") ); - let expected_panic: ByteArray = - "Only the world can init contract `test_contract_with_dojo_init_args`, but caller is `0`"; + let expected_panic: ByteArray = format!( + "Only the world can init contract `test_contract_with_dojo_init_args`, but caller is `2827`", + ); + + snf_utils::set_caller_address(starknet::contract_address_const::<2827>()); match starknet::syscalls::call_contract_syscall( address, dojo::world::world::DOJO_INIT_SELECTOR, [123].span() @@ -303,7 +317,7 @@ fn test_can_call_init_only_world_args() { // Remove the out of range error. s.pop_front().unwrap(); // Remove the ENTRYPOINT_FAILED suffix. - s.pop_back().unwrap(); + //s.pop_back().unwrap(); let e_str: ByteArray = Serde::deserialize(ref s).expect('failed deser'); diff --git a/crates/dojo/core-cairo-test/src/utils.cairo b/crates/dojo/core-foundry-test/src/utils.cairo similarity index 99% rename from crates/dojo/core-cairo-test/src/utils.cairo rename to crates/dojo/core-foundry-test/src/utils.cairo index 051be84e2a..71c89c1826 100644 --- a/crates/dojo/core-cairo-test/src/utils.cairo +++ b/crates/dojo/core-foundry-test/src/utils.cairo @@ -55,3 +55,4 @@ pub fn assert_array(value: Span, expected: Span) { i += 1; } } + diff --git a/crates/dojo/core-cairo-test/src/world.cairo b/crates/dojo/core-foundry-test/src/world.cairo similarity index 70% rename from crates/dojo/core-cairo-test/src/world.cairo rename to crates/dojo/core-foundry-test/src/world.cairo index 8e3910fc21..b38a9b08a9 100644 --- a/crates/dojo/core-cairo-test/src/world.cairo +++ b/crates/dojo/core-foundry-test/src/world.cairo @@ -1,25 +1,15 @@ use core::option::OptionTrait; -use core::result::ResultTrait; -use core::traits::{Into, TryInto}; -use starknet::{ContractAddress, syscalls::deploy_syscall}; +use crate::snf_utils; +use starknet::ContractAddress; -use dojo::world::{world, IWorldDispatcher, IWorldDispatcherTrait, WorldStorageTrait, WorldStorage}; +use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, WorldStorageTrait, WorldStorage}; -pub type TestClassHash = felt252; - -/// In Cairo test runner, all the classes are expected to be declared already. -/// If a contract belong to an other crate, it must be added to the `build-external-contract`, -/// event for testing, since Scarb does not do that automatically anymore. -/// -/// The [`TestResource`] enum uses a felt252 to represent the class hash, this avoids -/// having to write `bar::TEST_CLASS_HASH.try_into().unwrap()` in the test file, simply use -/// `bar::TEST_CLASS_HASH`. #[derive(Drop)] pub enum TestResource { - Event: TestClassHash, - Model: TestClassHash, - Contract: TestClassHash, + Event: ByteArray, + Model: ByteArray, + Contract: ByteArray, } #[derive(Drop, Copy)] @@ -95,36 +85,6 @@ pub impl ContractDefImpl of ContractDefTrait { } } -/// Deploy classhash with calldata for constructor -/// -/// # Arguments -/// -/// * `class_hash` - Class to deploy -/// * `calldata` - calldata for constructor -/// -/// # Returns -/// * address of contract deployed -pub fn deploy_contract(class_hash: felt252, calldata: Span) -> ContractAddress { - let (contract, _) = starknet::syscalls::deploy_syscall( - class_hash.try_into().unwrap(), 0, calldata, false - ) - .unwrap(); - 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 -pub fn deploy_with_world_address(class_hash: felt252, world: IWorldDispatcher) -> ContractAddress { - deploy_contract(class_hash, [world.contract_address.into()].span()) -} - /// Spawns a test world registering provided resources into namespaces. /// /// This function only deploys the world and registers the resources, it does not initialize the @@ -139,15 +99,8 @@ pub fn deploy_with_world_address(class_hash: felt252, world: IWorldDispatcher) - /// /// * World dispatcher pub fn spawn_test_world(namespaces_defs: Span) -> WorldStorage { - let salt = core::testing::get_available_gas(); - - let (world_address, _) = deploy_syscall( - world::TEST_CLASS_HASH.try_into().unwrap(), - salt.into(), - [world::TEST_CLASS_HASH].span(), - false - ) - .unwrap(); + let (world_contract, class_hash) = snf_utils::declare("world"); + let world_address = snf_utils::deploy(world_contract, @array![class_hash.into()]); let world = IWorldDispatcher { contract_address: world_address }; @@ -165,14 +118,18 @@ pub fn spawn_test_world(namespaces_defs: Span) -> WorldStorage { .resources .clone() { match r { - TestResource::Event(ch) => { - world.register_event(namespace.clone(), (*ch).try_into().unwrap()); + TestResource::Event(name) => { + let ch = snf_utils::declare_event_contract(name.clone()); + world.register_event(namespace.clone(), ch); }, - TestResource::Model(ch) => { - world.register_model(namespace.clone(), (*ch).try_into().unwrap()); + TestResource::Model(name) => { + let ch = snf_utils::declare_model_contract(name.clone()); + world.register_model(namespace.clone(), ch); }, - TestResource::Contract(ch) => { - world.register_contract(*ch, namespace.clone(), (*ch).try_into().unwrap()); + TestResource::Contract(name) => { + let (_, ch) = snf_utils::declare(name.clone()); + let salt = dojo::utils::bytearray_hash(name); + world.register_contract(salt, namespace.clone(), ch); } } } diff --git a/crates/dojo/core/Scarb.lock b/crates/dojo/core/Scarb.lock index b149ae24f4..ae6ebb7765 100644 --- a/crates/dojo/core/Scarb.lock +++ b/crates/dojo/core/Scarb.lock @@ -5,9 +5,9 @@ version = 1 name = "dojo" version = "1.0.3" dependencies = [ - "dojo_plugin", + "dojo_macros", ] [[package]] -name = "dojo_plugin" -version = "2.8.4" +name = "dojo_macros" +version = "0.1.0" diff --git a/crates/dojo/core/Scarb.toml b/crates/dojo/core/Scarb.toml index dd690edb16..188804abf8 100644 --- a/crates/dojo/core/Scarb.toml +++ b/crates/dojo/core/Scarb.toml @@ -7,8 +7,7 @@ version = "1.0.3" [dependencies] starknet = "=2.8.4" -dojo_plugin = { path = "../lang" } -#dojo_macros = { path = "../macros" } +dojo_macros = { path = "../macros" } [dev-dependencies] cairo_test = "=2.8.4" diff --git a/crates/dojo/core/src/utils/snf_test.cairo b/crates/dojo/core/src/utils/snf_test.cairo deleted file mode 100644 index 520e491c2e..0000000000 --- a/crates/dojo/core/src/utils/snf_test.cairo +++ /dev/null @@ -1,109 +0,0 @@ -use starknet::{ClassHash, ContractAddress}; -use snforge_std::{declare, ContractClassTrait, DeclareResultTrait}; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, Resource}; -use core::panics::panic_with_byte_array; - -#[derive(Drop)] -pub enum TestResource { - Event: ByteArray, - Model: ByteArray, - Contract: ByteArray, -} - -#[derive(Drop)] -pub struct NamespaceDef { - pub namespace: ByteArray, - pub resources: Span, -} - -/// Spawns a test world registering namespaces and resources. -/// -/// # Arguments -/// -/// * `namespaces` - Namespaces to register. -/// * `resources` - Resources to register. -/// -/// # Returns -/// -/// * World dispatcher -pub fn spawn_test_world(namespaces_defs: Span) -> IWorldDispatcher { - let world_contract = declare("world").unwrap().contract_class(); - let class_hash_felt: felt252 = (*world_contract.class_hash).into(); - let (world_address, _) = world_contract.deploy(@array![class_hash_felt]).unwrap(); - - let world = IWorldDispatcher { contract_address: world_address }; - - for ns in namespaces_defs { - let namespace = ns.namespace.clone(); - world.register_namespace(namespace.clone()); - - for r in ns - .resources - .clone() { - match r { - TestResource::Event(name) => { - let ch: ClassHash = *declare(name.clone()) - .unwrap() - .contract_class() - .class_hash; - world.register_event(namespace.clone(), ch); - }, - TestResource::Model(name) => { - let ch: ClassHash = *declare(name.clone()) - .unwrap() - .contract_class() - .class_hash; - world.register_model(namespace.clone(), ch); - }, - TestResource::Contract(name) => { - let ch: ClassHash = *declare(name.clone()) - .unwrap() - .contract_class() - .class_hash; - let salt = dojo::utils::bytearray_hash(name); - world.register_contract(salt, namespace.clone(), ch); - }, - } - } - }; - - world -} - -/// Extension trait for world dispatcher to test resources. -pub trait WorldTestExt { - fn resource_contract_address( - self: IWorldDispatcher, namespace: ByteArray, name: ByteArray - ) -> ContractAddress; - fn resource_class_hash( - self: IWorldDispatcher, namespace: ByteArray, name: ByteArray - ) -> ClassHash; -} - -impl WorldTestExtImpl of WorldTestExt { - fn resource_contract_address( - self: IWorldDispatcher, namespace: ByteArray, name: ByteArray - ) -> ContractAddress { - match self.resource(dojo::utils::selector_from_names(@namespace, @name)) { - Resource::Contract((ca, _)) => ca, - Resource::Event((ca, _)) => ca, - Resource::Model((ca, _)) => ca, - _ => panic_with_byte_array( - @format!("Resource is not registered: {}-{}", namespace, name) - ) - } - } - - fn resource_class_hash( - self: IWorldDispatcher, namespace: ByteArray, name: ByteArray - ) -> ClassHash { - match self.resource(dojo::utils::selector_from_names(@namespace, @name)) { - Resource::Contract((_, ch)) => ch.try_into().unwrap(), - Resource::Event((_, ch)) => ch.try_into().unwrap(), - Resource::Model((_, ch)) => ch.try_into().unwrap(), - _ => panic_with_byte_array( - @format!("Resource is not registered: {}-{}", namespace, name) - ), - } - } -} diff --git a/crates/dojo/lang/src/attribute_macros/contract.rs b/crates/dojo/lang/src/attribute_macros/contract.rs index 2f9c94c9b7..057f7f068b 100644 --- a/crates/dojo/lang/src/attribute_macros/contract.rs +++ b/crates/dojo/lang/src/attribute_macros/contract.rs @@ -35,32 +35,6 @@ impl DojoContract { module_ast: &ast::ItemModule, metadata: &MacroPluginMetadata<'_>, ) -> PluginResult { - let name = module_ast.name(db).text(db); - - let mut contract = DojoContract { diagnostics: vec![], systems: vec![] }; - - for (id, value) in [("name", &name.to_string())] { - if !naming::is_name_valid(value) { - return PluginResult { - code: None, - diagnostics: vec![PluginDiagnostic { - stable_ptr: module_ast.stable_ptr().0, - message: format!( - "The contract {id} '{value}' can only contain characters (a-z/A-Z), \ - digits (0-9) and underscore (_)." - ), - severity: Severity::Error, - }], - remove_original_item: false, - }; - } - } - - let mut has_event = false; - let mut has_storage = false; - let mut has_init = false; - let mut has_constructor = false; - if let MaybeModuleBody::Some(body) = module_ast.body(db) { let mut body_nodes: Vec<_> = body .iter_items_in_cfg(db, metadata.cfg_set) diff --git a/crates/dojo/macros/Cargo.toml b/crates/dojo/macros/Cargo.toml new file mode 100644 index 0000000000..6f0a0f8e33 --- /dev/null +++ b/crates/dojo/macros/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "dojo-macros" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +#cairo-lang-macro = {git = "https://github.com/software-mansion/scarb", rev="aff99810c37ceb77b61b3bd2ecee14a253a3397e"} +cairo-lang-macro = {path = "/Users/remybaranx/pro/projets/contribs/scarb/plugins/cairo-lang-macro" } +cairo-lang-defs.workspace = true +cairo-lang-parser.workspace = true +cairo-lang-plugins.workspace = true +cairo-lang-syntax.workspace = true +cairo-lang-utils.workspace = true +convert_case.workspace = true +dojo-types.workspace = true +serde.workspace = true +serde_json.workspace = true +starknet.workspace = true +starknet-crypto.workspace = true + +[dev-dependencies] +regex = "1.11.1" diff --git a/crates/dojo/macros/Scarb.lock b/crates/dojo/macros/Scarb.lock new file mode 100644 index 0000000000..2e5a11a7a4 --- /dev/null +++ b/crates/dojo/macros/Scarb.lock @@ -0,0 +1,6 @@ +# Code generated by scarb DO NOT EDIT. +version = 1 + +[[package]] +name = "dojo_macros" +version = "0.1.0" diff --git a/crates/dojo/macros/Scarb.toml b/crates/dojo/macros/Scarb.toml new file mode 100644 index 0000000000..17a88dd076 --- /dev/null +++ b/crates/dojo/macros/Scarb.toml @@ -0,0 +1,8 @@ +[package] +name = "dojo_macros" +version = "0.1.0" +description = "Dojo macros" +homepage = "https://github.com/dojoengine/dojo" +edition = "2024_07" + +[cairo-plugin] diff --git a/crates/dojo/macros/src/attributes/constants.rs b/crates/dojo/macros/src/attributes/constants.rs new file mode 100644 index 0000000000..4b9862c151 --- /dev/null +++ b/crates/dojo/macros/src/attributes/constants.rs @@ -0,0 +1,8 @@ +/// Dojo attribute names. +/// Note that, at the moment, these names must match with +/// proc macro function names. +pub const DOJO_CONTRACT_ATTR: &str = "dojo_contract"; +pub const DOJO_EVENT_ATTR: &str = "dojo_event"; +pub const DOJO_MODEL_ATTR: &str = "dojo_model"; + +pub const DOJO_ATTR_NAMES: [&str; 3] = [DOJO_CONTRACT_ATTR, DOJO_EVENT_ATTR, DOJO_MODEL_ATTR]; diff --git a/crates/dojo/macros/src/attributes/dojo_contract.rs b/crates/dojo/macros/src/attributes/dojo_contract.rs new file mode 100644 index 0000000000..d305b4e36f --- /dev/null +++ b/crates/dojo/macros/src/attributes/dojo_contract.rs @@ -0,0 +1,340 @@ +//! `dojo_contract` attribute macro. + +use cairo_lang_defs::patcher::{PatchBuilder, RewriteNode}; +use cairo_lang_macro::{attribute_macro, Diagnostic, Diagnostics, ProcMacroResult, TokenStream}; +use cairo_lang_parser::utils::SimpleParserDatabase; +use cairo_lang_syntax::node::ast::{MaybeModuleBody, OptionReturnTypeClause}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::BodyItems; +use cairo_lang_syntax::node::kind::SyntaxKind::ItemModule; +use cairo_lang_syntax::node::{ast, Terminal, TypedSyntaxNode}; +use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; + +use super::constants::DOJO_CONTRACT_ATTR; +use super::struct_parser::{validate_attributes, validate_namings_diagnostics}; +use crate::diagnostic_ext::DiagnosticsExt; + +const CONSTRUCTOR_FN: &str = "constructor"; +pub const DOJO_INIT_FN: &str = "dojo_init"; + +const CONTRACT_PATCH: &str = include_str!("./patches/contract.patch.cairo"); +const DEFAULT_INIT_PATCH: &str = include_str!("./patches/default_init.patch.cairo"); + +#[attribute_macro("dojo::contract")] +pub fn dojo_contract(_args: TokenStream, token_stream: TokenStream) -> ProcMacroResult { + handle_module_attribute_macro(token_stream) +} + +pub fn handle_module_attribute_macro(token_stream: TokenStream) -> ProcMacroResult { + let db = SimpleParserDatabase::default(); + let (root_node, _diagnostics) = db.parse_virtual_with_diagnostics(token_stream); + + for n in root_node.descendants(&db) { + // Process only the first module expected to be the contract. + if n.kind(&db) == ItemModule { + let module_ast = ast::ItemModule::from_syntax_node(&db, n); + return from_module(&db, &module_ast); + } + } + + ProcMacroResult::new(TokenStream::empty()) +} + +pub fn from_module(db: &dyn SyntaxGroup, module_ast: &ast::ItemModule) -> ProcMacroResult { + let name = module_ast.name(db).text(db); + + let mut diagnostics = vec![]; + + diagnostics.extend(validate_attributes(db, &module_ast.attributes(db), DOJO_CONTRACT_ATTR)); + + diagnostics.extend(validate_namings_diagnostics(&[("contract name", &name)])); + + let mut has_event = false; + let mut has_storage = false; + let mut has_init = false; + let mut has_constructor = false; + + if let MaybeModuleBody::Some(body) = module_ast.body(db) { + // TODO: Use `.iter_items_in_cfg(db, metadata.cfg_set)` when possible + // to ensure we don't loop on items that are not in the current cfg set. + let mut body_nodes: Vec<_> = body + .items_vec(db) + .iter() + .flat_map(|el| { + if let ast::ModuleItem::Enum(ref enum_ast) = el { + if enum_ast.name(db).text(db).to_string() == "Event" { + has_event = true; + + return merge_event(db, enum_ast.clone()); + } + } else if let ast::ModuleItem::Struct(ref struct_ast) = el { + if struct_ast.name(db).text(db).to_string() == "Storage" { + has_storage = true; + return merge_storage(db, struct_ast.clone()); + } + } else if let ast::ModuleItem::FreeFunction(ref fn_ast) = el { + let fn_decl = fn_ast.declaration(db); + let fn_name = fn_decl.name(db).text(db); + + if fn_name == CONSTRUCTOR_FN { + has_constructor = true; + return handle_constructor_fn(db, fn_ast); + } + + if fn_name == DOJO_INIT_FN { + has_init = true; + return handle_init_fn(db, fn_ast, &mut diagnostics); + } + } + + vec![RewriteNode::Copied(el.as_syntax_node())] + }) + .collect(); + + if !has_constructor { + let node = RewriteNode::Text( + " + #[constructor] + fn constructor(ref self: ContractState) { + self.world_provider.initializer(); + } + " + .to_string(), + ); + + body_nodes.append(&mut vec![node]); + } + + if !has_init { + let node = RewriteNode::interpolate_patched( + DEFAULT_INIT_PATCH, + &UnorderedHashMap::from([( + "init_name".to_string(), + RewriteNode::Text(DOJO_INIT_FN.to_string()), + )]), + ); + body_nodes.append(&mut vec![node]); + } + + if !has_event { + body_nodes.append(&mut create_event()) + } + + if !has_storage { + body_nodes.append(&mut create_storage()) + } + + let mut builder = PatchBuilder::new(db, module_ast); + builder.add_modified(RewriteNode::Mapped { + node: Box::new(RewriteNode::interpolate_patched( + CONTRACT_PATCH, + &UnorderedHashMap::from([ + ("name".to_string(), RewriteNode::Text(name.to_string())), + ("body".to_string(), RewriteNode::new_modified(body_nodes)), + ]), + )), + origin: module_ast.as_syntax_node().span_without_trivia(db), + }); + + let (code, _) = builder.build(); + + crate::debug_expand(&format!("CONTRACT PATCH: {name}"), &code); + + return ProcMacroResult::new(TokenStream::new(code)) + .with_diagnostics(Diagnostics::new(diagnostics)); + } + + ProcMacroResult::new(TokenStream::empty()) +} +/// If a constructor is provided, we should keep the user statements. +/// We only inject the world provider initializer. +fn handle_constructor_fn(db: &dyn SyntaxGroup, fn_ast: &ast::FunctionWithBody) -> Vec { + let fn_decl = fn_ast.declaration(db); + + let params_str = params_to_str(db, fn_decl.signature(db).parameters(db)); + + let declaration_node = RewriteNode::Mapped { + node: Box::new(RewriteNode::Text(format!( + " + #[constructor] + fn constructor({}) {{ + self.world_provider.initializer(); + ", + params_str + ))), + origin: fn_ast.declaration(db).as_syntax_node().span_without_trivia(db), + }; + + let func_nodes = fn_ast + .body(db) + .statements(db) + .elements(db) + .iter() + .map(|e| RewriteNode::Mapped { + node: Box::new(RewriteNode::from(e.as_syntax_node())), + origin: e.as_syntax_node().span_without_trivia(db), + }) + .collect::>(); + + let mut nodes = vec![declaration_node]; + + nodes.extend(func_nodes); + + // Close the constructor with users statements included. + nodes.push(RewriteNode::Text("}\n".to_string())); + + nodes +} + +fn handle_init_fn( + db: &dyn SyntaxGroup, + fn_ast: &ast::FunctionWithBody, + diagnostics: &mut Vec, +) -> Vec { + let fn_decl = fn_ast.declaration(db); + + if let OptionReturnTypeClause::ReturnTypeClause(_) = fn_decl.signature(db).ret_ty(db) { + diagnostics.push_error(format!("The {} function cannot have a return type.", DOJO_INIT_FN)); + } + + let params: Vec = fn_decl + .signature(db) + .parameters(db) + .elements(db) + .iter() + .map(|p| p.as_syntax_node().get_text(db)) + .collect::>(); + + let params_str = params.join(", "); + + // Since the dojo init is meant to be called by the world, we don't need an + // interface to be generated (which adds a considerable amount of code). + let impl_node = RewriteNode::Text( + " + #[abi(per_item)] + #[generate_trait] + pub impl IDojoInitImpl of IDojoInit { + #[external(v0)] + " + .to_string(), + ); + + let declaration_node = RewriteNode::Mapped { + node: Box::new(RewriteNode::Text(format!("fn {}({}) {{", DOJO_INIT_FN, params_str))), + origin: fn_ast.declaration(db).as_syntax_node().span_without_trivia(db), + }; + + // Asserts the caller is the world, and close the init function. + let assert_world_caller_node = RewriteNode::Text( + "if starknet::get_caller_address() != \ + self.world_provider.world_dispatcher().contract_address { \ + core::panics::panic_with_byte_array(@format!(\"Only the world can init contract `{}`, \ + but caller is `{:?}`\", self.dojo_name(), starknet::get_caller_address())); }" + .to_string(), + ); + + let func_nodes = fn_ast + .body(db) + .statements(db) + .elements(db) + .iter() + .map(|e| RewriteNode::Mapped { + node: Box::new(RewriteNode::from(e.as_syntax_node())), + origin: e.as_syntax_node().span_without_trivia(db), + }) + .collect::>(); + + let mut nodes = vec![impl_node, declaration_node, assert_world_caller_node]; + nodes.extend(func_nodes); + // Close the init function + close the impl block. + nodes.push(RewriteNode::Text("}\n}".to_string())); + + nodes +} + +pub fn merge_event(db: &dyn SyntaxGroup, enum_ast: ast::ItemEnum) -> Vec { + let mut rewrite_nodes = vec![]; + + 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(",\n"); + + rewrite_nodes.push(RewriteNode::interpolate_patched( + " + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: upgradeable_cpt::Event, + WorldProviderEvent: world_provider_cpt::Event, + $variants$ + } + ", + &UnorderedHashMap::from([("variants".to_string(), RewriteNode::Text(variants))]), + )); + rewrite_nodes +} + +pub fn create_event() -> Vec { + vec![RewriteNode::Text( + " + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: upgradeable_cpt::Event, + WorldProviderEvent: world_provider_cpt::Event, + } + " + .to_string(), + )] +} + +pub fn merge_storage(db: &dyn SyntaxGroup, struct_ast: ast::ItemStruct) -> Vec { + let mut rewrite_nodes = vec![]; + + let elements = struct_ast.members(db).elements(db); + + let members = elements.iter().map(|e| e.as_syntax_node().get_text(db)).collect::>(); + let members = members.join(",\n"); + + rewrite_nodes.push(RewriteNode::interpolate_patched( + " + #[storage] + struct Storage { + #[substorage(v0)] + upgradeable: upgradeable_cpt::Storage, + #[substorage(v0)] + world_provider: world_provider_cpt::Storage, + $members$ + } + ", + &UnorderedHashMap::from([("members".to_string(), RewriteNode::Text(members))]), + )); + rewrite_nodes +} + +pub fn create_storage() -> Vec { + vec![RewriteNode::Text( + " + #[storage] + struct Storage { + #[substorage(v0)] + upgradeable: upgradeable_cpt::Storage, + #[substorage(v0)] + world_provider: world_provider_cpt::Storage, + } + " + .to_string(), + )] +} + +/// Converts parameter list to it's string representation. +pub fn params_to_str(db: &dyn SyntaxGroup, param_list: ast::ParamList) -> String { + let params = param_list + .elements(db) + .iter() + .map(|param| param.as_syntax_node().get_text(db)) + .collect::>(); + + params.join(", ") +} diff --git a/crates/dojo/macros/src/attributes/dojo_event.rs b/crates/dojo/macros/src/attributes/dojo_event.rs new file mode 100644 index 0000000000..d3c16d20a8 --- /dev/null +++ b/crates/dojo/macros/src/attributes/dojo_event.rs @@ -0,0 +1,124 @@ +//! `dojo_event` attribute macro. + +use cairo_lang_defs::patcher::{PatchBuilder, RewriteNode}; +use cairo_lang_macro::{attribute_macro, Diagnostics, ProcMacroResult, TokenStream}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; +use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; + +use super::constants::DOJO_EVENT_ATTR; +use super::struct_parser::{ + compute_unique_hash, handle_struct_attribute_macro, parse_members, serialize_keys_and_values, + validate_attributes, validate_namings_diagnostics, +}; +use crate::attributes::struct_parser::remove_derives; +use crate::derives::{extract_derive_attr_names, DOJO_INTROSPECT_DERIVE, DOJO_PACKED_DERIVE}; +use crate::diagnostic_ext::DiagnosticsExt; + +const EVENT_PATCH: &str = include_str!("./patches/event.patch.cairo"); + +#[attribute_macro("dojo::event")] +pub fn dojo_event(_args: TokenStream, token_stream: TokenStream) -> ProcMacroResult { + handle_event_attribute_macro(token_stream) +} + +// inner function to be called in tests as `dojo_event()` is automatically renamed +// by the `attribute_macro` processing +pub fn handle_event_attribute_macro(token_stream: TokenStream) -> ProcMacroResult { + handle_struct_attribute_macro(token_stream, from_struct) +} + +pub fn from_struct(db: &dyn SyntaxGroup, struct_ast: &ast::ItemStruct) -> ProcMacroResult { + let mut diagnostics = vec![]; + + let event_name = struct_ast.name(db).as_syntax_node().get_text(db).trim().to_string(); + + diagnostics.extend(validate_attributes(db, &struct_ast.attributes(db), DOJO_EVENT_ATTR)); + + diagnostics.extend(validate_namings_diagnostics(&[("event name", &event_name)])); + + let members = parse_members(db, &struct_ast.members(db).elements(db), &mut diagnostics); + + let mut serialized_keys: Vec = vec![]; + let mut serialized_values: Vec = vec![]; + + serialize_keys_and_values(&members, &mut serialized_keys, &mut serialized_values); + + if serialized_keys.is_empty() { + diagnostics.push_error("Event must define at least one #[key] attribute".to_string()); + } + + if serialized_values.is_empty() { + diagnostics + .push_error("Event must define at least one member that is not a key".to_string()); + } + + let members_values = members + .iter() + .filter_map(|m| { + if m.key { + None + } else { + Some(RewriteNode::Text(format!("pub {}: {},\n", m.name, m.ty))) + } + }) + .collect::>(); + + let member_names = members + .iter() + .map(|member| RewriteNode::Text(format!("{},\n", member.name.clone()))) + .collect::>(); + + let derive_attr_names = extract_derive_attr_names( + db, + &mut diagnostics, + struct_ast.attributes(db).query_attr(db, "derive"), + ); + + if derive_attr_names.contains(&DOJO_PACKED_DERIVE.to_string()) { + diagnostics.push_error(format!("Deriving {DOJO_PACKED_DERIVE} on event is not allowed.")); + } + + let has_drop = derive_attr_names.contains(&"Drop".to_string()); + let has_serde = derive_attr_names.contains(&"Serde".to_string()); + + if !has_drop || !has_serde { + diagnostics.push_error("Event must derive from Drop and Serde.".to_string()); + } + + // Ensures events always derive Introspect if not already derived. + let derive_node = RewriteNode::Text(format!("#[derive({})]", DOJO_INTROSPECT_DERIVE)); + + // Must remove the derives from the original struct since they would create duplicates + // with the derives of other plugins. + let original_struct = remove_derives(db, struct_ast); + + let unique_hash = + compute_unique_hash(db, &event_name, false, &struct_ast.members(db).elements(db)) + .to_string(); + + let dojo_node = RewriteNode::interpolate_patched( + EVENT_PATCH, + &UnorderedHashMap::from([ + ("derive_node".to_string(), derive_node), + ("original_struct".to_string(), original_struct), + ("type_name".to_string(), RewriteNode::Text(event_name.clone())), + ("member_names".to_string(), RewriteNode::new_modified(member_names)), + ("serialized_keys".to_string(), RewriteNode::new_modified(serialized_keys)), + ("serialized_values".to_string(), RewriteNode::new_modified(serialized_values)), + ("unique_hash".to_string(), RewriteNode::Text(unique_hash)), + ("members_values".to_string(), RewriteNode::new_modified(members_values)), + ]), + ); + + let mut builder = PatchBuilder::new(db, struct_ast); + builder.add_modified(dojo_node); + + let (code, _) = builder.build(); + + crate::debug_expand(&format!("EVENT PATCH: {event_name}"), &code); + + return ProcMacroResult::new(TokenStream::new(code)) + .with_diagnostics(Diagnostics::new(diagnostics)); +} diff --git a/crates/dojo/macros/src/attributes/dojo_model.rs b/crates/dojo/macros/src/attributes/dojo_model.rs new file mode 100644 index 0000000000..06f256f258 --- /dev/null +++ b/crates/dojo/macros/src/attributes/dojo_model.rs @@ -0,0 +1,195 @@ +//! `dojo_model` attribute macro. + +use cairo_lang_defs::patcher::{PatchBuilder, RewriteNode}; +use cairo_lang_macro::{attribute_macro, Diagnostics, ProcMacroResult, TokenStream}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; +use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; +use starknet::core::utils::get_selector_from_name; + +use super::constants::DOJO_MODEL_ATTR; +use super::struct_parser::{ + compute_unique_hash, handle_struct_attribute_macro, parse_members, serialize_member_ty, + validate_attributes, validate_namings_diagnostics, Member, +}; +use crate::attributes::struct_parser::remove_derives; +use crate::derives::{extract_derive_attr_names, DOJO_INTROSPECT_DERIVE, DOJO_PACKED_DERIVE}; +use crate::diagnostic_ext::DiagnosticsExt; + +const MODEL_CODE_PATCH: &str = include_str!("./patches/model.patch.cairo"); +const MODEL_FIELD_CODE_PATCH: &str = include_str!("./patches/model_field_store.patch.cairo"); + +#[attribute_macro("dojo::model")] +pub fn dojo_model(_args: TokenStream, token_stream: TokenStream) -> ProcMacroResult { + handle_model_attribute_macro(token_stream) +} + +// inner function to be called in tests as `dojo_model()` is automatically renamed +// by the `attribute_macro` processing +pub fn handle_model_attribute_macro(token_stream: TokenStream) -> ProcMacroResult { + handle_struct_attribute_macro(token_stream, from_struct) +} + +pub fn from_struct(db: &dyn SyntaxGroup, struct_ast: &ast::ItemStruct) -> ProcMacroResult { + let mut diagnostics = vec![]; + + let model_type = struct_ast.name(db).as_syntax_node().get_text(db).trim().to_string(); + + diagnostics.extend(validate_attributes(db, &struct_ast.attributes(db), DOJO_MODEL_ATTR)); + diagnostics.extend(validate_namings_diagnostics(&[("model name", &model_type)])); + + let mut values: Vec = vec![]; + let mut keys: Vec = vec![]; + let mut members_values: Vec = vec![]; + let mut key_types: Vec = vec![]; + let mut key_attrs: Vec = vec![]; + + let mut serialized_keys: Vec = vec![]; + let mut serialized_values: Vec = vec![]; + let mut field_accessors: Vec = vec![]; + + let members = parse_members(db, &struct_ast.members(db).elements(db), &mut diagnostics); + + members.iter().for_each(|member| { + if member.key { + keys.push(member.clone()); + key_types.push(member.ty.clone()); + key_attrs.push(format!("*self.{}", member.name.clone())); + serialized_keys.push(serialize_member_ty(member, true)); + } else { + values.push(member.clone()); + serialized_values.push(serialize_member_ty(member, true)); + members_values + .push(RewriteNode::Text(format!("pub {}: {},\n", member.name, member.ty))); + field_accessors.push(generate_field_accessors(model_type.clone(), member)); + } + }); + + if keys.is_empty() { + diagnostics.push_error("Model must define at least one #[key] attribute".to_string()); + } + + if values.is_empty() { + diagnostics + .push_error("Model must define at least one member that is not a key".to_string()); + } + + if !diagnostics.is_empty() { + return ProcMacroResult::new(TokenStream::empty()) + .with_diagnostics(Diagnostics::new(diagnostics)); + } + + let (keys_to_tuple, key_type) = if keys.len() > 1 { + (format!("({})", key_attrs.join(", ")), format!("({})", key_types.join(", "))) + } else { + (key_attrs.first().unwrap().to_string(), key_types.first().unwrap().to_string()) + }; + + let derive_attr_names = extract_derive_attr_names( + db, + &mut diagnostics, + struct_ast.attributes(db).query_attr(db, "derive"), + ); + + let has_introspect = derive_attr_names.contains(&DOJO_INTROSPECT_DERIVE.to_string()); + let has_introspect_packed = derive_attr_names.contains(&DOJO_PACKED_DERIVE.to_string()); + let has_drop = derive_attr_names.contains(&"Drop".to_string()); + let has_serde = derive_attr_names.contains(&"Serde".to_string()); + + if has_introspect && has_introspect_packed { + diagnostics.push_error( + "Model cannot derive from both Introspect and IntrospectPacked.".to_string(), + ); + } + + #[allow(clippy::nonminimal_bool)] + if !has_drop || !has_serde { + diagnostics.push_error("Model must derive from Drop and Serde.".to_string()); + } + + let derive_node = if has_introspect_packed { + RewriteNode::Text(format!("#[derive({})]", DOJO_PACKED_DERIVE)) + } else { + RewriteNode::Text(format!("#[derive({})]", DOJO_INTROSPECT_DERIVE)) + }; + + // Must remove the derives from the original struct since they would create duplicates + // with the derives of other plugins. + let original_struct = remove_derives(db, struct_ast); + + // Reuse the same derive attributes for ModelValue (except Introspect/IntrospectPacked). + let model_value_derive_attr_names = derive_attr_names + .iter() + .map(|d| d.as_str()) + .filter(|&d| d != DOJO_INTROSPECT_DERIVE && d != DOJO_PACKED_DERIVE) + .collect::>() + .join(", "); + + let unique_hash = compute_unique_hash( + db, + &model_type, + has_introspect_packed, + &struct_ast.members(db).elements(db), + ) + .to_string(); + + let dojo_node = RewriteNode::interpolate_patched( + MODEL_CODE_PATCH, + &UnorderedHashMap::from([ + ("derive_node".to_string(), derive_node), + ("original_struct".to_string(), original_struct), + ("model_type".to_string(), RewriteNode::Text(model_type.clone())), + ("serialized_keys".to_string(), RewriteNode::new_modified(serialized_keys)), + ("serialized_values".to_string(), RewriteNode::new_modified(serialized_values)), + ("keys_to_tuple".to_string(), RewriteNode::Text(keys_to_tuple)), + ("key_type".to_string(), RewriteNode::Text(key_type)), + ("members_values".to_string(), RewriteNode::new_modified(members_values)), + ("field_accessors".to_string(), RewriteNode::new_modified(field_accessors)), + ( + "model_value_derive_attr_names".to_string(), + RewriteNode::Text(model_value_derive_attr_names), + ), + ("unique_hash".to_string(), RewriteNode::Text(unique_hash)), + ]), + ); + + let mut builder = PatchBuilder::new(db, struct_ast); + builder.add_modified(dojo_node); + + let (code, _) = builder.build(); + + crate::debug_expand(&format!("MODEL PATCH: {model_type}"), &code); + + return ProcMacroResult::new(TokenStream::new(code)) + .with_diagnostics(Diagnostics::new(diagnostics)); +} + +/// Generates field accessors (`get_[field_name]` and `set_[field_name]`) for every +/// fields of a model. +/// +/// # Arguments +/// +/// * `model_name` - the model name. +/// * `param_keys` - coma separated model keys with the format `KEY_NAME: KEY_TYPE`. +/// * `serialized_param_keys` - code to serialize model keys in a `serialized` felt252 array. +/// * `member` - information about the field for which to generate accessors. +/// +/// # Returns +/// A [`RewriteNode`] containing accessors code. +fn generate_field_accessors(model_type: String, member: &Member) -> RewriteNode { + RewriteNode::interpolate_patched( + MODEL_FIELD_CODE_PATCH, + &UnorderedHashMap::from([ + ("model_type".to_string(), RewriteNode::Text(model_type)), + ( + "field_selector".to_string(), + RewriteNode::Text( + get_selector_from_name(&member.name).expect("invalid member name").to_string(), + ), + ), + ("field_name".to_string(), RewriteNode::Text(member.name.clone())), + ("field_type".to_string(), RewriteNode::Text(member.ty.clone())), + ]), + ) +} diff --git a/crates/dojo/macros/src/attributes/mod.rs b/crates/dojo/macros/src/attributes/mod.rs new file mode 100644 index 0000000000..a243e83ac9 --- /dev/null +++ b/crates/dojo/macros/src/attributes/mod.rs @@ -0,0 +1,5 @@ +pub mod constants; +pub mod dojo_contract; +pub mod dojo_event; +pub mod dojo_model; +pub mod struct_parser; diff --git a/crates/dojo/macros/src/attributes/patches/contract.patch.cairo b/crates/dojo/macros/src/attributes/patches/contract.patch.cairo new file mode 100644 index 0000000000..46ee7353b1 --- /dev/null +++ b/crates/dojo/macros/src/attributes/patches/contract.patch.cairo @@ -0,0 +1,35 @@ +#[starknet::contract] +pub mod $name$ { + use dojo::contract::components::world_provider::{world_provider_cpt, world_provider_cpt::InternalTrait as WorldProviderInternal, IWorldProvider}; + use dojo::contract::components::upgradeable::upgradeable_cpt; + use dojo::contract::IContract; + use dojo::meta::IDeployedResource; + + component!(path: world_provider_cpt, storage: world_provider, event: WorldProviderEvent); + component!(path: upgradeable_cpt, storage: upgradeable, event: UpgradeableEvent); + + #[abi(embed_v0)] + impl WorldProviderImpl = world_provider_cpt::WorldProviderImpl; + + #[abi(embed_v0)] + impl UpgradeableImpl = upgradeable_cpt::UpgradeableImpl; + + #[abi(embed_v0)] + pub impl $name$__ContractImpl of IContract {} + + #[abi(embed_v0)] + pub impl $name$__DeployedContractImpl of IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "$name$" + } + } + + #[generate_trait] + impl $name$InternalImpl of $name$InternalTrait { + fn world(self: @ContractState, namespace: @ByteArray) -> dojo::world::storage::WorldStorage { + dojo::world::WorldStorageTrait::new(self.world_provider.world_dispatcher(), namespace) + } + } + + $body$ +} diff --git a/crates/dojo/macros/src/attributes/patches/default_init.patch.cairo b/crates/dojo/macros/src/attributes/patches/default_init.patch.cairo new file mode 100644 index 0000000000..435bad567e --- /dev/null +++ b/crates/dojo/macros/src/attributes/patches/default_init.patch.cairo @@ -0,0 +1,14 @@ +#[abi(per_item)] +#[generate_trait] +pub impl IDojoInitImpl of IDojoInit { + #[external(v0)] + fn $init_name$(self: @ContractState) { + if starknet::get_caller_address() != self.world_provider.world_dispatcher().contract_address { + core::panics::panic_with_byte_array( + @format!("Only the world can init contract `{}`, but caller is `{:?}`", + self.dojo_name(), + starknet::get_caller_address(), + )); + } + } +} diff --git a/crates/dojo/macros/src/attributes/patches/event.patch.cairo b/crates/dojo/macros/src/attributes/patches/event.patch.cairo new file mode 100644 index 0000000000..a6275ab39c --- /dev/null +++ b/crates/dojo/macros/src/attributes/patches/event.patch.cairo @@ -0,0 +1,76 @@ +$derive_node$ +$original_struct$ + +// EventValue on it's own does nothing since events are always emitted and +// never read from the storage. However, it's required by the ABI to +// ensure that the event definition contains both keys and values easily distinguishable. +// Only derives strictly required traits. +#[derive(Drop, Serde)] +pub struct $type_name$Value { + $members_values$ +} + +pub impl $type_name$Definition of dojo::event::EventDefinition<$type_name$>{ + #[inline(always)] + fn name() -> ByteArray { + "$type_name$" + } +} + +pub impl $type_name$ModelParser of dojo::model::model::ModelParser<$type_name$>{ + fn serialize_keys(self: @$type_name$) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + $serialized_keys$ + core::array::ArrayTrait::span(@serialized) + } + fn serialize_values(self: @$type_name$) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + $serialized_values$ + core::array::ArrayTrait::span(@serialized) + } +} + +pub impl $type_name$EventImpl = dojo::event::event::EventImpl<$type_name$>; + +#[starknet::contract] +pub mod e_$type_name$ { + use super::$type_name$; + use super::$type_name$Value; + + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl $type_name$__DeployedEventImpl = dojo::event::component::IDeployedEventImpl; + + #[abi(embed_v0)] + impl $type_name$__StoredEventImpl = dojo::event::component::IStoredEventImpl; + + #[abi(embed_v0)] + impl $type_name$__EventImpl = dojo::event::component::IEventImpl; + + #[abi(per_item)] + #[generate_trait] + impl $type_name$Impl of I$type_name${ + // Ensures the ABI contains the Event struct, since it's never used + // by systems directly. + #[external(v0)] + fn ensure_abi(self: @ContractState, event: $type_name$) { + let _event = event; + } + + // Outputs EventValue to allow a simple diff from the ABI compared to the + // event to retrieved the keys of an event. + #[external(v0)] + fn ensure_values(self: @ContractState, value: $type_name$Value) { + let _value = value; + } + + // Ensures the generated contract has a unique classhash, using + // a hardcoded hash computed on event and member names. + #[external(v0)] + fn ensure_unique(self: @ContractState) { + let _hash = $unique_hash$; + } + } +} diff --git a/crates/dojo/macros/src/attributes/patches/model.patch.cairo b/crates/dojo/macros/src/attributes/patches/model.patch.cairo new file mode 100644 index 0000000000..77002bfaea --- /dev/null +++ b/crates/dojo/macros/src/attributes/patches/model.patch.cairo @@ -0,0 +1,120 @@ +$derive_node$ +$original_struct$ + +#[derive($model_value_derive_attr_names$)] +pub struct $model_type$Value { + $members_values$ +} + +type $model_type$KeyType = $key_type$; + +pub impl $model_type$KeyParser of dojo::model::model::KeyParser<$model_type$, $model_type$KeyType>{ + #[inline(always)] + fn parse_key(self: @$model_type$) -> $model_type$KeyType { + $keys_to_tuple$ + } +} + +impl $model_type$ModelValueKey of dojo::model::model_value::ModelValueKey<$model_type$Value, $model_type$KeyType> { +} + +// Impl to get the static definition of a model +pub mod m_$model_type$_definition { + use super::$model_type$; + pub impl $model_type$DefinitionImpl of dojo::model::ModelDefinition{ + #[inline(always)] + fn name() -> ByteArray { + "$model_type$" + } + + #[inline(always)] + fn layout() -> dojo::meta::Layout { + dojo::meta::Introspect::<$model_type$>::layout() + } + + #[inline(always)] + fn schema() -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(s) = dojo::meta::Introspect::<$model_type$>::ty() { + s + } + else { + panic!("Model `$model_type$`: invalid schema.") + } + } + + #[inline(always)] + fn size() -> Option { + dojo::meta::Introspect::<$model_type$>::size() + } + } +} + +pub impl $model_type$Definition = m_$model_type$_definition::$model_type$DefinitionImpl<$model_type$>; +pub impl $model_type$ModelValueDefinition = m_$model_type$_definition::$model_type$DefinitionImpl<$model_type$Value>; + +pub impl $model_type$ModelParser of dojo::model::model::ModelParser<$model_type$>{ + fn serialize_keys(self: @$model_type$) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + $serialized_keys$ + core::array::ArrayTrait::span(@serialized) + } + fn serialize_values(self: @$model_type$) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + $serialized_values$ + core::array::ArrayTrait::span(@serialized) + } +} + +pub impl $model_type$ModelValueParser of dojo::model::model_value::ModelValueParser<$model_type$Value>{ + fn serialize_values(self: @$model_type$Value) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + $serialized_values$ + core::array::ArrayTrait::span(@serialized) + } +} + +pub impl $model_type$ModelImpl = dojo::model::model::ModelImpl<$model_type$>; +pub impl $model_type$ModelValueImpl = dojo::model::model_value::ModelValueImpl<$model_type$Value>; + +#[starknet::contract] +pub mod m_$model_type$ { + use super::$model_type$; + use super::$model_type$Value; + + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl $model_type$__DojoDeployedModelImpl = dojo::model::component::IDeployedModelImpl; + + #[abi(embed_v0)] + impl $model_type$__DojoStoredModelImpl = dojo::model::component::IStoredModelImpl; + + #[abi(embed_v0)] + impl $model_type$__DojoModelImpl = dojo::model::component::IModelImpl; + + #[abi(per_item)] + #[generate_trait] + impl $model_type$Impl of I$model_type${ + // Ensures the ABI contains the Model struct, even if never used + // into as a system input. + #[external(v0)] + fn ensure_abi(self: @ContractState, model: $model_type$) { + let _model = model; + } + + // Outputs ModelValue to allow a simple diff from the ABI compared to the + // model to retrieved the keys of a model. + #[external(v0)] + fn ensure_values(self: @ContractState, value: $model_type$Value) { + let _value = value; + } + + // Ensures the generated contract has a unique classhash, using + // a hardcoded hash computed on model and member names. + #[external(v0)] + fn ensure_unique(self: @ContractState) { + let _hash = $unique_hash$; + } + } +} diff --git a/crates/dojo/macros/src/attributes/patches/model_field_store.patch.cairo b/crates/dojo/macros/src/attributes/patches/model_field_store.patch.cairo new file mode 100644 index 0000000000..8fa466617f --- /dev/null +++ b/crates/dojo/macros/src/attributes/patches/model_field_store.patch.cairo @@ -0,0 +1,15 @@ + fn get_$field_name$(self: @S, key: $model_type$KeyType) -> $field_type$ { + $model_type$Store::get_member(self, key, $field_selector$) + } + + fn get_$field_name$_from_id(self: @S, entity_id: felt252) -> $field_type$ { + $model_type$ModelValueStore::get_member_from_id(self, entity_id, $field_selector$) + } + + fn update_$field_name$(ref self: S, key: $model_type$KeyType, value: $field_type$) { + $model_type$Store::update_member(ref self, key, $field_selector$, value); + } + + fn update_$field_name$_from_id(ref self: S, entity_id: felt252, value: $field_type$) { + $model_type$ModelValueStore::update_member_from_id(ref self, entity_id, $field_selector$, value); + } diff --git a/crates/dojo/macros/src/attributes/struct_parser.rs b/crates/dojo/macros/src/attributes/struct_parser.rs new file mode 100644 index 0000000000..dcb25abe47 --- /dev/null +++ b/crates/dojo/macros/src/attributes/struct_parser.rs @@ -0,0 +1,214 @@ +use cairo_lang_defs::patcher::RewriteNode; +use cairo_lang_macro::{Diagnostic, ProcMacroResult, Severity, TokenStream}; +use cairo_lang_parser::utils::SimpleParserDatabase; +use cairo_lang_syntax::node::ast::{self, AttributeList, Member as MemberAst}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::kind::SyntaxKind::ItemStruct; +use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode}; +use dojo_types::naming; +use dojo_types::naming::compute_bytearray_hash; +use serde::{Deserialize, Serialize}; +use starknet_crypto::{poseidon_hash_many, Felt}; + +use super::constants::DOJO_ATTR_NAMES; +use crate::diagnostic_ext::DiagnosticsExt; + +/// Represents a member of a struct. +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct Member { + // Name of the member. + pub name: String, + // Type of the member. + pub ty: String, + // Whether the member is a key. + pub key: bool, +} + +pub fn parse_members( + db: &dyn SyntaxGroup, + members: &[MemberAst], + diagnostics: &mut Vec, +) -> Vec { + members + .iter() + .filter_map(|member_ast| { + let member = Member { + name: member_ast.name(db).text(db).to_string(), + ty: member_ast + .type_clause(db) + .ty(db) + .as_syntax_node() + .get_text(db) + .trim() + .to_string(), + key: member_ast.has_attr(db, "key"), + }; + + // validate key member + if member.key && member.ty == "u256" { + diagnostics.push(Diagnostic { + message: "Key is only supported for core types that are 1 felt long once \ + serialized. `u256` is a struct of 2 u128, hence not supported." + .into(), + severity: Severity::Error, + }); + None + } else { + Some(member) + } + }) + .collect::>() +} + +pub fn serialize_keys_and_values( + members: &[Member], + serialized_keys: &mut Vec, + serialized_values: &mut Vec, +) { + members.iter().for_each(|member| { + if member.key { + serialized_keys.push(serialize_member_ty(member, true)); + } else { + serialized_values.push(serialize_member_ty(member, true)); + } + }); +} + +/// Creates a [`RewriteNode`] for the member type serialization. +/// +/// # Arguments +/// +/// * member: The member to serialize. +pub fn serialize_member_ty(member: &Member, with_self: bool) -> RewriteNode { + RewriteNode::Text(format!( + "core::serde::Serde::serialize({}{}, ref serialized);\n", + if with_self { "self." } else { "@" }, + member.name + )) +} + +pub fn deserialize_member_ty(member: &Member, input_name: &str) -> RewriteNode { + RewriteNode::Text(format!( + "let {} = core::serde::Serde::<{}>::deserialize(ref {input_name})?;\n", + member.name, member.ty + )) +} + +/// Validates the namings of the attributes. +/// +/// # Arguments +/// +/// * namings: A list of tuples containing the id and value of the attribute. +/// +/// # Returns +/// +/// A vector of diagnostics. +pub fn validate_namings_diagnostics(namings: &[(&str, &str)]) -> Vec { + let mut diagnostics = vec![]; + + for (id, value) in namings { + if !naming::is_name_valid(value) { + diagnostics.push_error(format!( + "The {id} '{value}' can only contain characters (a-z/A-Z), digits (0-9) and \ + underscore (_)." + )); + } + } + + diagnostics +} + +/// Removes the derives from the original struct. +pub fn remove_derives(db: &dyn SyntaxGroup, struct_ast: &ast::ItemStruct) -> RewriteNode { + let mut out_lines = vec![]; + + let struct_str = struct_ast.as_syntax_node().get_text_without_trivia(db).to_string(); + + for l in struct_str.lines() { + if !l.starts_with("#[derive") { + out_lines.push(l); + } + } + + RewriteNode::Text(out_lines.join("\n")) +} + +/// Validates the attributes of a Dojo attribute. +/// +/// Parameters: +/// * db: The semantic database. +/// * module_ast: The AST of the contract module. +/// +/// Returns: +/// * A vector of diagnostics. +pub fn validate_attributes( + db: &dyn SyntaxGroup, + attribute_list: &AttributeList, + ref_attribute: &str, +) -> Vec { + let mut diagnostics = vec![]; + + for attribute in DOJO_ATTR_NAMES { + if attribute == ref_attribute { + if attribute_list.query_attr(db, attribute).first().is_some() { + diagnostics.push_error(format!( + "Only one {} attribute is allowed per module.", + ref_attribute + )); + } + } else { + if attribute_list.query_attr(db, attribute).first().is_some() { + diagnostics.push_error(format!( + "A {} can't be used together with a {}.", + ref_attribute, attribute + )); + } + } + } + + diagnostics +} + +/// Compute a unique hash based on the element name and types and names of members. +/// This hash is used in element contracts to ensure uniqueness. +pub fn compute_unique_hash( + db: &dyn SyntaxGroup, + element_name: &str, + is_packed: bool, + members: &[MemberAst], +) -> Felt { + let mut hashes = + vec![if is_packed { Felt::ONE } else { Felt::ZERO }, compute_bytearray_hash(element_name)]; + hashes.extend( + members + .iter() + .map(|m| { + poseidon_hash_many(&[ + compute_bytearray_hash(&m.name(db).text(db).to_string()), + compute_bytearray_hash( + m.type_clause(db).ty(db).as_syntax_node().get_text(db).trim(), + ), + ]) + }) + .collect::>(), + ); + poseidon_hash_many(&hashes) +} + +pub fn handle_struct_attribute_macro( + token_stream: TokenStream, + from_struct: fn(&dyn SyntaxGroup, &ast::ItemStruct) -> ProcMacroResult, +) -> ProcMacroResult { + let db = SimpleParserDatabase::default(); + let (root_node, _diagnostics) = db.parse_virtual_with_diagnostics(token_stream); + + for n in root_node.descendants(&db) { + if n.kind(&db) == ItemStruct { + let struct_ast = ast::ItemStruct::from_syntax_node(&db, n); + return from_struct(&db, &struct_ast); + } + } + + ProcMacroResult::new(TokenStream::empty()) +} diff --git a/crates/dojo/macros/src/derives/introspect/layout.rs b/crates/dojo/macros/src/derives/introspect/layout.rs new file mode 100644 index 0000000000..c38b5d8042 --- /dev/null +++ b/crates/dojo/macros/src/derives/introspect/layout.rs @@ -0,0 +1,338 @@ +use cairo_lang_macro::Diagnostic; +use cairo_lang_syntax::node::ast::{Expr, ItemEnum, ItemStruct, OptionTypeClause, TypeClause}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode}; +use starknet::core::utils::get_selector_from_name; + +use super::utils::{ + get_array_item_type, get_tuple_item_types, is_array, is_byte_array, is_tuple, + is_unsupported_option_type, primitive_type_introspection, +}; +use crate::diagnostic_ext::DiagnosticsExt; + +/// build the full layout for every field in the Struct. +pub fn build_field_layouts( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + struct_ast: &ItemStruct, +) -> String { + struct_ast + .members(db) + .elements(db) + .iter() + .filter_map(|m| { + if m.has_attr(db, "key") { + return None; + } + + let field_name = m.name(db).text(db); + let field_selector = get_selector_from_name(&field_name.to_string()).unwrap(); + let field_layout = get_layout_from_type_clause(db, diagnostics, &m.type_clause(db)); + Some(format!( + "dojo::meta::FieldLayout {{ + selector: {field_selector}, + layout: {field_layout} + }}" + )) + }) + .collect::>() + .join(",\n") +} + +/// build the full layout for every variant in the Enum. +/// Note that every variant may have a different associated data type. +pub fn build_variant_layouts( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + enum_ast: &ItemEnum, +) -> String { + enum_ast + .variants(db) + .elements(db) + .iter() + .enumerate() + .map(|(i, v)| { + let selector = format!("{i}"); + + let variant_layout = match v.type_clause(db) { + OptionTypeClause::Empty(_) => { + "dojo::meta::Layout::Fixed(array![].span())".to_string() + } + OptionTypeClause::TypeClause(type_clause) => { + get_layout_from_type_clause(db, diagnostics, &type_clause) + } + }; + + format!( + "dojo::meta::FieldLayout {{ + selector: {selector}, + layout: {variant_layout} + }}" + ) + }) + .collect::>() + .join(",\n") +} + +/// Build a field layout describing the provided type clause. +pub fn get_layout_from_type_clause( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + type_clause: &TypeClause, +) -> String { + match type_clause.ty(db) { + Expr::Path(path) => { + let path_type = path.as_syntax_node().get_text(db); + build_item_layout_from_type(diagnostics, &path_type) + } + Expr::Tuple(expr) => { + let tuple_type = expr.as_syntax_node().get_text(db); + build_tuple_layout_from_type(diagnostics, &tuple_type) + } + _ => { + diagnostics.push_error("Unexpected expression for variant data type.".to_string()); + "ERROR".to_string() + } + } +} + +/// Build the array layout describing the provided array type. +/// item_type could be something like `Array` for example. +pub fn build_array_layout_from_type(diagnostics: &mut Vec, item_type: &str) -> String { + let array_item_type = get_array_item_type(item_type); + + if is_tuple(&array_item_type) { + format!( + "dojo::meta::Layout::Array( + array![ + {} + ].span() + )", + build_item_layout_from_type(diagnostics, &array_item_type) + ) + } else if is_array(&array_item_type) { + format!( + "dojo::meta::Layout::Array( + array![ + {} + ].span() + )", + build_array_layout_from_type(diagnostics, &array_item_type) + ) + } else { + format!("dojo::meta::introspect::Introspect::<{}>::layout()", item_type) + } +} + +/// Build the tuple layout describing the provided tuple type. +/// item_type could be something like (u8, u32, u128) for example. +pub fn build_tuple_layout_from_type(diagnostics: &mut Vec, item_type: &str) -> String { + let tuple_items = get_tuple_item_types(item_type) + .iter() + .map(|x| build_item_layout_from_type(diagnostics, x)) + .collect::>() + .join(",\n"); + format!( + "dojo::meta::Layout::Tuple( + array![ + {} + ].span() + )", + tuple_items + ) +} + +/// Build the layout describing the provided type. +/// item_type could be any type (array, tuple, struct, ...) +pub fn build_item_layout_from_type(diagnostics: &mut Vec, item_type: &str) -> String { + if is_array(item_type) { + build_array_layout_from_type(diagnostics, item_type) + } else if is_tuple(item_type) { + build_tuple_layout_from_type(diagnostics, item_type) + } else { + // For Option, T cannot be a tuple + if is_unsupported_option_type(item_type) { + diagnostics.push_error( + "Option cannot be used with tuples. Prefer using a struct.".to_string(), + ); + } + + format!("dojo::meta::introspect::Introspect::<{}>::layout()", item_type) + } +} + +pub fn is_custom_layout(layout: &str) -> bool { + layout.starts_with("dojo::meta::introspect::Introspect::") +} + +pub fn build_packed_struct_layout( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + struct_ast: &ItemStruct, +) -> String { + let layouts = struct_ast + .members(db) + .elements(db) + .iter() + .filter_map(|m| { + if m.has_attr(db, "key") { + return None; + } + + Some(get_packed_field_layout_from_type_clause(db, diagnostics, &m.type_clause(db))) + }) + .flatten() + .collect::>(); + + if layouts.iter().any(|v| is_custom_layout(v.as_str())) { + generate_cairo_code_for_fixed_layout_with_custom_types(&layouts) + } else { + format!( + "dojo::meta::Layout::Fixed( + array![ + {} + ].span() + )", + layouts.join(",") + ) + } +} + +pub fn generate_cairo_code_for_fixed_layout_with_custom_types(layouts: &[String]) -> String { + let layouts_repr = layouts + .iter() + .map(|l| { + if is_custom_layout(l) { + l.to_string() + } else { + format!("dojo::meta::Layout::Fixed(array![{l}].span())") + } + }) + .collect::>() + .join(",\n"); + + format!( + "let mut layouts = array![ + {layouts_repr} + ]; + let mut merged_layout = ArrayTrait::::new(); + + loop {{ + match ArrayTrait::pop_front(ref layouts) {{ + Option::Some(mut layout) => {{ + match layout {{ + dojo::meta::Layout::Fixed(mut l) => {{ + loop {{ + match SpanTrait::pop_front(ref l) {{ + Option::Some(x) => merged_layout.append(*x), + Option::None(_) => {{ break; }} + }}; + }}; + }}, + _ => panic!(\"A packed model layout must contain Fixed layouts only.\"), + }}; + }}, + Option::None(_) => {{ break; }} + }}; + }}; + + dojo::meta::Layout::Fixed(merged_layout.span()) + ", + ) +} + +// +pub fn build_packed_enum_layout( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + enum_ast: &ItemEnum, +) -> String { + // to be packable, all variants data must have the same size. + // as this point has already been checked before calling `build_packed_enum_layout`, + // just use the first variant to generate the fixed layout. + let elements = enum_ast.variants(db).elements(db); + let mut variant_layout = if elements.is_empty() { + vec![] + } else { + match elements.first().unwrap().type_clause(db) { + OptionTypeClause::Empty(_) => vec![], + OptionTypeClause::TypeClause(type_clause) => { + get_packed_field_layout_from_type_clause(db, diagnostics, &type_clause) + } + } + }; + + // don't forget the store the variant value + variant_layout.insert(0, "8".to_string()); + + if variant_layout.iter().any(|v| is_custom_layout(v.as_str())) { + generate_cairo_code_for_fixed_layout_with_custom_types(&variant_layout) + } else { + format!( + "dojo::meta::Layout::Fixed( + array![ + {} + ].span() + )", + variant_layout.join(",") + ) + } +} + +// +pub fn get_packed_field_layout_from_type_clause( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + type_clause: &TypeClause, +) -> Vec { + match type_clause.ty(db) { + Expr::Path(path) => { + let path_type = path.as_syntax_node().get_text(db); + get_packed_item_layout_from_type(diagnostics, path_type.trim()) + } + Expr::Tuple(expr) => { + let tuple_type = expr.as_syntax_node().get_text(db); + get_packed_tuple_layout_from_type(diagnostics, &tuple_type) + } + _ => { + diagnostics.push_error("Unexpected expression for variant data type.".to_string()); + vec!["ERROR".to_string()] + } + } +} + +// +pub fn get_packed_item_layout_from_type( + diagnostics: &mut Vec, + item_type: &str, +) -> Vec { + if is_array(item_type) || is_byte_array(item_type) { + diagnostics.push_error("Array field cannot be packed.".to_string()); + vec!["ERROR".to_string()] + } else if is_tuple(item_type) { + get_packed_tuple_layout_from_type(diagnostics, item_type) + } else { + let primitives = primitive_type_introspection(); + + if let Some(p) = primitives.get(item_type) { + vec![p.1.iter().map(|x| x.to_string()).collect::>().join(",")] + } else { + // as we cannot verify that an enum/struct custom type is packable, + // we suppose it is and let the user verify this. + // If it's not the case, the Dojo model layout function will panic. + vec![format!("dojo::meta::introspect::Introspect::<{}>::layout()", item_type)] + } + } +} + +// +pub fn get_packed_tuple_layout_from_type( + diagnostics: &mut Vec, + item_type: &str, +) -> Vec { + get_tuple_item_types(item_type) + .iter() + .flat_map(|x| get_packed_item_layout_from_type(diagnostics, x)) + .collect::>() +} diff --git a/crates/dojo/macros/src/derives/introspect/mod.rs b/crates/dojo/macros/src/derives/introspect/mod.rs new file mode 100644 index 0000000000..e01c4e4cd2 --- /dev/null +++ b/crates/dojo/macros/src/derives/introspect/mod.rs @@ -0,0 +1,154 @@ +use cairo_lang_defs::patcher::RewriteNode; +use cairo_lang_macro::Diagnostic; +use cairo_lang_syntax::node::ast::{ + GenericParam, ItemEnum, ItemStruct, OptionWrappedGenericParamList, +}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::Terminal; +use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; + +use crate::diagnostic_ext::DiagnosticsExt; + +mod layout; +mod size; +mod ty; +mod utils; + +/// Generate the introspect of a Struct +pub fn handle_introspect_struct( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + struct_ast: ItemStruct, + packed: bool, +) -> RewriteNode { + let struct_name = struct_ast.name(db).text(db).into(); + let struct_size = size::compute_struct_layout_size(db, &struct_ast, packed); + let ty = ty::build_struct_ty(db, &struct_name, &struct_ast); + + let layout = if packed { + layout::build_packed_struct_layout(db, diagnostics, &struct_ast) + } else { + format!( + "dojo::meta::Layout::Struct( + array![ + {} + ].span() + )", + layout::build_field_layouts(db, diagnostics, &struct_ast) + ) + }; + + let (gen_types, gen_impls) = build_generic_types_and_impls(db, struct_ast.generic_params(db)); + + generate_introspect(&struct_name, &struct_size, &gen_types, gen_impls, &layout, &ty) +} + +/// Generate the introspect of a Enum +pub fn handle_introspect_enum( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + enum_ast: ItemEnum, + packed: bool, +) -> RewriteNode { + let enum_name = enum_ast.name(db).text(db).into(); + let variant_sizes = size::compute_enum_variant_sizes(db, &enum_ast); + + let layout = if packed { + if size::is_enum_packable(&variant_sizes) { + layout::build_packed_enum_layout(db, diagnostics, &enum_ast) + } else { + diagnostics.push_error( + "To be packed, all variants must have fixed layout of same size.".to_string(), + ); + "ERROR".to_string() + } + } else { + format!( + "dojo::meta::Layout::Enum( + array![ + {} + ].span() + )", + layout::build_variant_layouts(db, diagnostics, &enum_ast) + ) + }; + + let (gen_types, gen_impls) = build_generic_types_and_impls(db, enum_ast.generic_params(db)); + let enum_size = size::compute_enum_layout_size(&variant_sizes, packed); + let ty = ty::build_enum_ty(db, &enum_name, &enum_ast); + + generate_introspect(&enum_name, &enum_size, &gen_types, gen_impls, &layout, &ty) +} + +/// Generate the introspect impl for a Struct or an Enum, +/// based on its name, size, layout and Ty. +fn generate_introspect( + name: &String, + size: &String, + generic_types: &[String], + generic_impls: String, + layout: &String, + ty: &String, +) -> RewriteNode { + RewriteNode::interpolate_patched( + " +impl $name$Introspect<$generics$> of dojo::meta::introspect::Introspect<$name$<$generics_types$>> \ + { + #[inline(always)] + fn size() -> Option { + $size$ + } + + fn layout() -> dojo::meta::Layout { + $layout$ + } + + #[inline(always)] + fn ty() -> dojo::meta::introspect::Ty { + $ty$ + } +} + ", + &UnorderedHashMap::from([ + ("name".to_string(), RewriteNode::Text(name.to_string())), + ("generics".to_string(), RewriteNode::Text(generic_impls)), + ("generics_types".to_string(), RewriteNode::Text(generic_types.join(", "))), + ("size".to_string(), RewriteNode::Text(size.to_string())), + ("layout".to_string(), RewriteNode::Text(layout.to_string())), + ("ty".to_string(), RewriteNode::Text(ty.to_string())), + ]), + ) +} + +// Extract generic type information and build the +// type and impl information to add to the generated introspect +fn build_generic_types_and_impls( + db: &dyn SyntaxGroup, + generic_params: OptionWrappedGenericParamList, +) -> (Vec, String) { + let generic_types = + if let OptionWrappedGenericParamList::WrappedGenericParamList(params) = generic_params { + params + .generic_params(db) + .elements(db) + .iter() + .filter_map(|el| { + if let GenericParam::Type(typ) = el { + Some(typ.name(db).text(db).to_string()) + } else { + None + } + }) + .collect::>() + } else { + vec![] + }; + + let generic_impls = generic_types + .iter() + .map(|g| format!("{g}, impl {g}Introspect: dojo::meta::introspect::Introspect<{g}>")) + .collect::>() + .join(", "); + + (generic_types, generic_impls) +} diff --git a/crates/dojo/macros/src/derives/introspect/size.rs b/crates/dojo/macros/src/derives/introspect/size.rs new file mode 100644 index 0000000000..efb81ea25d --- /dev/null +++ b/crates/dojo/macros/src/derives/introspect/size.rs @@ -0,0 +1,200 @@ +use cairo_lang_syntax::node::ast::{Expr, ItemEnum, ItemStruct, OptionTypeClause, TypeClause}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::TypedSyntaxNode; + +use super::utils::{ + get_tuple_item_types, is_array, is_byte_array, is_tuple, primitive_type_introspection, +}; + +pub fn compute_struct_layout_size( + db: &dyn SyntaxGroup, + struct_ast: &ItemStruct, + is_packed: bool, +) -> String { + let mut cumulated_sizes = 0; + let mut is_dynamic_size = false; + + let mut sizes = struct_ast + .members(db) + .elements(db) + .into_iter() + .filter_map(|m| { + if m.has_attr(db, "key") { + return None; + } + + let (sizes, cumulated, is_dynamic) = + get_field_size_from_type_clause(db, &m.type_clause(db)); + + cumulated_sizes += cumulated; + is_dynamic_size |= is_dynamic; + Some(sizes) + }) + .flatten() + .collect::>(); + build_size_function_body(&mut sizes, cumulated_sizes, is_dynamic_size, is_packed) +} + +pub fn compute_enum_variant_sizes( + db: &dyn SyntaxGroup, + enum_ast: &ItemEnum, +) -> Vec<(Vec, u32, bool)> { + enum_ast + .variants(db) + .elements(db) + .iter() + .map(|v| match v.type_clause(db) { + OptionTypeClause::Empty(_) => (vec![], 0, false), + OptionTypeClause::TypeClause(type_clause) => { + get_field_size_from_type_clause(db, &type_clause) + } + }) + .collect::>() +} + +pub fn is_enum_packable(variant_sizes: &[(Vec, u32, bool)]) -> bool { + if variant_sizes.is_empty() { + return true; + } + + let v0_sizes = variant_sizes[0].0.clone(); + let v0_fixed_size = variant_sizes[0].1; + + variant_sizes.iter().all(|vs| { + vs.0.len() == v0_sizes.len() + && vs.0.iter().zip(v0_sizes.iter()).all(|(a, b)| a == b) + && vs.1 == v0_fixed_size + && !vs.2 + }) +} + +pub fn compute_enum_layout_size( + variant_sizes: &[(Vec, u32, bool)], + is_packed: bool, +) -> String { + if variant_sizes.is_empty() { + return "Option::None".to_string(); + } + + let v0 = variant_sizes[0].clone(); + let identical_variants = + variant_sizes.iter().all(|vs| vs.0 == v0.0 && vs.1 == v0.1 && vs.2 == v0.2); + + if identical_variants { + let (mut sizes, mut cumulated_sizes, is_dynamic_size) = v0; + + // add one felt252 to store the variant identifier + cumulated_sizes += 1; + + build_size_function_body(&mut sizes, cumulated_sizes, is_dynamic_size, is_packed) + } else { + "Option::None".to_string() + } +} + +pub fn build_size_function_body( + sizes: &mut Vec, + cumulated_sizes: u32, + is_dynamic_size: bool, + is_packed: bool, +) -> String { + if is_dynamic_size { + return "Option::None".to_string(); + } + + if cumulated_sizes > 0 { + sizes.push(format!("Option::Some({})", cumulated_sizes)); + } + + match sizes.len() { + 0 => "Option::None".to_string(), + 1 => sizes[0].clone(), + _ => { + let none_check = if is_packed { + "" + } else { + "if dojo::utils::any_none(@sizes) { + return Option::None; + }" + }; + + format!( + "let sizes : Array> = array![ + {} + ]; + + {none_check} + Option::Some(dojo::utils::sum(sizes)) + ", + sizes.join(",\n") + ) + } + } +} + +pub fn get_field_size_from_type_clause( + db: &dyn SyntaxGroup, + type_clause: &TypeClause, +) -> (Vec, u32, bool) { + let mut cumulated_sizes = 0; + let mut is_dynamic_size = false; + + let field_sizes = match type_clause.ty(db) { + Expr::Path(path) => { + let path_type = path.as_syntax_node().get_text(db).trim().to_string(); + compute_item_size_from_type(&path_type) + } + Expr::Tuple(expr) => { + let tuple_type = expr.as_syntax_node().get_text(db).trim().to_string(); + compute_tuple_size_from_type(&tuple_type) + } + _ => { + // field type already checked while building the layout + vec!["ERROR".to_string()] + } + }; + + let sizes = field_sizes + .into_iter() + .filter_map(|s| match s.parse::() { + Ok(v) => { + cumulated_sizes += v; + None + } + Err(_) => { + if s.eq("Option::None") { + is_dynamic_size = true; + None + } else { + Some(s) + } + } + }) + .collect::>(); + + (sizes, cumulated_sizes, is_dynamic_size) +} + +pub fn compute_item_size_from_type(item_type: &String) -> Vec { + if is_array(item_type) || is_byte_array(item_type) { + vec!["Option::None".to_string()] + } else if is_tuple(item_type) { + compute_tuple_size_from_type(item_type) + } else { + let primitives = primitive_type_introspection(); + + if let Some(p) = primitives.get(item_type) { + vec![p.0.to_string()] + } else { + vec![format!("dojo::meta::introspect::Introspect::<{}>::size()", item_type)] + } + } +} + +pub fn compute_tuple_size_from_type(tuple_type: &str) -> Vec { + get_tuple_item_types(tuple_type) + .iter() + .flat_map(compute_item_size_from_type) + .collect::>() +} diff --git a/crates/dojo/macros/src/derives/introspect/ty.rs b/crates/dojo/macros/src/derives/introspect/ty.rs new file mode 100644 index 0000000000..d9e9e40a11 --- /dev/null +++ b/crates/dojo/macros/src/derives/introspect/ty.rs @@ -0,0 +1,133 @@ +use cairo_lang_syntax::node::ast::{ + Expr, ItemEnum, ItemStruct, Member, OptionTypeClause, TypeClause, Variant, +}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode}; + +use super::utils::{get_array_item_type, get_tuple_item_types, is_array, is_byte_array, is_tuple}; + +pub fn build_struct_ty(db: &dyn SyntaxGroup, name: &String, struct_ast: &ItemStruct) -> String { + let members_ty = struct_ast + .members(db) + .elements(db) + .iter() + .map(|m| build_member_ty(db, m)) + .collect::>(); + + format!( + "dojo::meta::introspect::Ty::Struct( + dojo::meta::introspect::Struct {{ + name: '{name}', + attrs: array![].span(), + children: array![ + {}\n + ].span() + }} + )", + members_ty.join(",\n") + ) +} + +pub fn build_enum_ty(db: &dyn SyntaxGroup, name: &String, enum_ast: &ItemEnum) -> String { + let variants = enum_ast.variants(db).elements(db); + + let variants_ty = if variants.is_empty() { + "".to_string() + } else { + variants.iter().map(|v| build_variant_ty(db, v)).collect::>().join(",\n") + }; + + format!( + "dojo::meta::introspect::Ty::Enum( + dojo::meta::introspect::Enum {{ + name: '{name}', + attrs: array![].span(), + children: array![ + {variants_ty}\n + ].span() + }} + )" + ) +} + +pub fn build_member_ty(db: &dyn SyntaxGroup, member: &Member) -> String { + let name = member.name(db).text(db).to_string(); + let attrs = if member.has_attr(db, "key") { vec!["'key'"] } else { vec![] }; + + format!( + "dojo::meta::introspect::Member {{ + name: '{name}', + attrs: array![{}].span(), + ty: {} + }}", + attrs.join(","), + build_ty_from_type_clause(db, &member.type_clause(db)) + ) +} + +pub fn build_variant_ty(db: &dyn SyntaxGroup, variant: &Variant) -> String { + let name = variant.name(db).text(db).to_string(); + match variant.type_clause(db) { + OptionTypeClause::Empty(_) => { + // use an empty tuple if the variant has no data + format!("('{name}', dojo::meta::introspect::Ty::Tuple(array![].span()))") + } + OptionTypeClause::TypeClause(type_clause) => { + format!("('{name}', {})", build_ty_from_type_clause(db, &type_clause)) + } + } +} + +pub fn build_ty_from_type_clause(db: &dyn SyntaxGroup, type_clause: &TypeClause) -> String { + match type_clause.ty(db) { + Expr::Path(path) => { + let path_type = path.as_syntax_node().get_text(db).trim().to_string(); + build_item_ty_from_type(&path_type) + } + Expr::Tuple(expr) => { + let tuple_type = expr.as_syntax_node().get_text(db).trim().to_string(); + build_tuple_ty_from_type(&tuple_type) + } + _ => { + // diagnostic message already handled in layout building + "ERROR".to_string() + } + } +} + +pub fn build_item_ty_from_type(item_type: &String) -> String { + if is_array(item_type) { + let array_item_type = get_array_item_type(item_type); + format!( + "dojo::meta::introspect::Ty::Array( + array![ + {} + ].span() + )", + build_item_ty_from_type(&array_item_type) + ) + } else if is_byte_array(item_type) { + "dojo::meta::introspect::Ty::ByteArray".to_string() + } else if is_tuple(item_type) { + build_tuple_ty_from_type(item_type) + } else { + format!("dojo::meta::introspect::Introspect::<{}>::ty()", item_type) + } +} + +pub fn build_tuple_ty_from_type(item_type: &str) -> String { + let tuple_items = get_tuple_item_types(item_type) + .iter() + .map(build_item_ty_from_type) + .collect::>() + .join(",\n"); + format!( + "dojo::meta::introspect::Ty::Tuple( + array![ + {} + ].span() + )", + tuple_items + ) +} diff --git a/crates/dojo/macros/src/derives/introspect/utils.rs b/crates/dojo/macros/src/derives/introspect/utils.rs new file mode 100644 index 0000000000..f57f6b6335 --- /dev/null +++ b/crates/dojo/macros/src/derives/introspect/utils.rs @@ -0,0 +1,136 @@ +use std::collections::HashMap; + +#[derive(Clone, Default, Debug)] +pub struct TypeIntrospection(pub usize, pub Vec); + +// Provides type introspection information for primitive types +pub fn primitive_type_introspection() -> HashMap { + HashMap::from([ + ("bytes31".into(), TypeIntrospection(1, vec![248])), + ("felt252".into(), TypeIntrospection(1, vec![251])), + ("bool".into(), TypeIntrospection(1, vec![1])), + ("u8".into(), TypeIntrospection(1, vec![8])), + ("u16".into(), TypeIntrospection(1, vec![16])), + ("u32".into(), TypeIntrospection(1, vec![32])), + ("u64".into(), TypeIntrospection(1, vec![64])), + ("u128".into(), TypeIntrospection(1, vec![128])), + ("u256".into(), TypeIntrospection(2, vec![128, 128])), + ("usize".into(), TypeIntrospection(1, vec![32])), + ("ContractAddress".into(), TypeIntrospection(1, vec![251])), + ("ClassHash".into(), TypeIntrospection(1, vec![251])), + ]) +} + +/// Check if the provided type is an unsupported `Option`, +/// because tuples are not supported with Option. +pub fn is_unsupported_option_type(ty: &str) -> bool { + ty.starts_with("Option<(") +} + +pub fn is_byte_array(ty: &str) -> bool { + ty.eq("ByteArray") +} + +pub fn is_array(ty: &str) -> bool { + ty.starts_with("Array<") || ty.starts_with("Span<") +} + +pub fn is_tuple(ty: &str) -> bool { + ty.starts_with('(') +} + +pub fn get_array_item_type(ty: &str) -> String { + if ty.starts_with("Array<") { + ty.trim().strip_prefix("Array<").unwrap().strip_suffix('>').unwrap().to_string() + } else { + ty.trim().strip_prefix("Span<").unwrap().strip_suffix('>').unwrap().to_string() + } +} + +/// split a tuple in array of items (nested tuples are not splitted). +/// example (u8, (u16, u32), u128) -> ["u8", "(u16, u32)", "u128"] +pub fn get_tuple_item_types(ty: &str) -> Vec { + let tuple_str = ty + .trim() + .strip_prefix('(') + .unwrap() + .strip_suffix(')') + .unwrap() + .to_string() + .replace(' ', ""); + let mut items = vec![]; + let mut current_item = "".to_string(); + let mut level = 0; + + for c in tuple_str.chars() { + if c == ',' { + if level > 0 { + current_item.push(c); + } + + if level == 0 && !current_item.is_empty() { + items.push(current_item); + current_item = "".to_string(); + } + } else { + current_item.push(c); + + if c == '(' { + level += 1; + } + if c == ')' { + level -= 1; + } + } + } + + if !current_item.is_empty() { + items.push(current_item); + } + + items +} + +#[test] +pub fn test_get_tuple_item_types() { + pub fn assert_array(got: Vec, expected: Vec) { + pub fn format_array(arr: Vec) -> String { + format!("[{}]", arr.join(", ")) + } + + assert!( + got.len() == expected.len(), + "arrays have not the same length (got: {}, expected: {})", + format_array(got), + format_array(expected) + ); + + for i in 0..got.len() { + assert!( + got[i] == expected[i], + "unexpected array item: (got: {} expected: {})", + got[i], + expected[i] + ) + } + } + + let test_cases = vec![ + ("(u8,)", vec!["u8"]), + ("(u8, u16, u32)", vec!["u8", "u16", "u32"]), + ("(u8, (u16,), u32)", vec!["u8", "(u16,)", "u32"]), + ("(u8, (u16, (u8, u16)))", vec!["u8", "(u16,(u8,u16))"]), + ("(Array<(Points, Damage)>, ((u16,),)))", vec!["Array<(Points,Damage)>", "((u16,),))"]), + ( + "(u8, (u16, (u8, u16), Array<(Points, Damage)>), ((u16,),)))", + vec!["u8", "(u16,(u8,u16),Array<(Points,Damage)>)", "((u16,),))"], + ), + ]; + + for (value, expected) in test_cases { + assert_array( + get_tuple_item_types(value), + expected.iter().map(|x| x.to_string()).collect::>(), + ) + } +} diff --git a/crates/dojo/macros/src/derives/mod.rs b/crates/dojo/macros/src/derives/mod.rs new file mode 100644 index 0000000000..bbc018703f --- /dev/null +++ b/crates/dojo/macros/src/derives/mod.rs @@ -0,0 +1,230 @@ +//! Derive macros. +//! +//! A derive macros is a macro that is used to generate code generally for a struct or enum. +//! The input of the macro consists of the AST of the struct or enum and the attributes of the +//! derive macro. + +use cairo_lang_defs::patcher::{PatchBuilder, RewriteNode}; +use cairo_lang_macro::{derive_macro, Diagnostic, Diagnostics, ProcMacroResult, TokenStream}; +use cairo_lang_parser::utils::SimpleParserDatabase; +use cairo_lang_syntax::attribute::structured::{AttributeArgVariant, AttributeStructurize}; +use cairo_lang_syntax::node::ast::Attribute; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::helpers::QueryAttrs; +use cairo_lang_syntax::node::kind::SyntaxKind::{ItemEnum, ItemStruct}; +use cairo_lang_syntax::node::{ast, Terminal, TypedSyntaxNode}; +use introspect::{handle_introspect_enum, handle_introspect_struct}; +use print::{handle_print_enum, handle_print_struct}; + +use crate::diagnostic_ext::DiagnosticsExt; + +pub mod introspect; +pub mod print; + +pub const DOJO_PRINT_DERIVE: &str = "Print"; +pub const DOJO_INTROSPECT_DERIVE: &str = "Introspect"; +pub const DOJO_PACKED_DERIVE: &str = "IntrospectPacked"; + +#[derive_macro] +fn introspect(token_stream: TokenStream) -> ProcMacroResult { + handle_derives_macros(token_stream) +} + +#[derive_macro] +fn introspect_packed(token_stream: TokenStream) -> ProcMacroResult { + handle_derives_macros(token_stream) +} + +pub fn handle_derives_macros(token_stream: TokenStream) -> ProcMacroResult { + let db = SimpleParserDatabase::default(); + let (syn_file, _diagnostics) = db.parse_virtual_with_diagnostics(token_stream); + + for n in syn_file.descendants(&db) { + // Process only the first module expected to be the contract. + return match n.kind(&db) { + ItemStruct => { + let struct_ast = ast::ItemStruct::from_syntax_node(&db, n); + let attrs = struct_ast.attributes(&db).query_attr(&db, "derive"); + + dojo_derive_all(&db, attrs, &ast::ModuleItem::Struct(struct_ast)) + } + ItemEnum => { + let enum_ast = ast::ItemEnum::from_syntax_node(&db, n); + let attrs = enum_ast.attributes(&db).query_attr(&db, "derive"); + + dojo_derive_all(&db, attrs, &ast::ModuleItem::Enum(enum_ast)) + } + _ => { + continue; + } + }; + } + + ProcMacroResult::new(TokenStream::empty()) +} + +/// Handles all the dojo derives macro and returns the generated code and diagnostics. +pub fn dojo_derive_all( + db: &dyn SyntaxGroup, + attrs: Vec, + item_ast: &ast::ModuleItem, +) -> ProcMacroResult { + if attrs.is_empty() { + return ProcMacroResult::new(TokenStream::empty()); + } + + let mut diagnostics = vec![]; + + let derive_attr_names = extract_derive_attr_names(db, &mut diagnostics, attrs); + + let (rewrite_nodes, derive_diagnostics) = handle_derive_attrs(db, &derive_attr_names, item_ast); + + diagnostics.extend(derive_diagnostics); + + let mut builder = PatchBuilder::new(db, item_ast); + for node in rewrite_nodes { + builder.add_modified(node); + } + + let (code, _) = builder.build(); + + let item_name = item_ast.as_syntax_node().get_text_without_trivia(db).to_string(); + + crate::debug_expand(&format!("DERIVE {}", item_name), &code.to_string()); + + return ProcMacroResult::new(TokenStream::new(code)) + .with_diagnostics(Diagnostics::new(diagnostics)); +} + +/// Handles the derive attributes of a struct or enum. +pub fn handle_derive_attrs( + db: &dyn SyntaxGroup, + attrs: &[String], + item_ast: &ast::ModuleItem, +) -> (Vec, Vec) { + let mut rewrite_nodes = Vec::new(); + let mut diagnostics = Vec::new(); + + check_for_derive_attr_conflicts(&mut diagnostics, attrs); + + match item_ast { + ast::ModuleItem::Struct(struct_ast) => { + for a in attrs { + match a.as_str() { + DOJO_PRINT_DERIVE => { + rewrite_nodes.push(handle_print_struct(db, struct_ast.clone())); + } + DOJO_INTROSPECT_DERIVE => { + rewrite_nodes.push(handle_introspect_struct( + db, + &mut diagnostics, + struct_ast.clone(), + false, + )); + } + DOJO_PACKED_DERIVE => { + rewrite_nodes.push(handle_introspect_struct( + db, + &mut diagnostics, + struct_ast.clone(), + true, + )); + } + _ => continue, + } + } + } + ast::ModuleItem::Enum(enum_ast) => { + for a in attrs { + match a.as_str() { + DOJO_PRINT_DERIVE => { + rewrite_nodes.push(handle_print_enum(db, enum_ast.clone())); + } + DOJO_INTROSPECT_DERIVE => { + rewrite_nodes.push(handle_introspect_enum( + db, + &mut diagnostics, + enum_ast.clone(), + false, + )); + } + DOJO_PACKED_DERIVE => { + rewrite_nodes.push(handle_introspect_enum( + db, + &mut diagnostics, + enum_ast.clone(), + true, + )); + } + _ => continue, + } + } + } + _ => { + // Currently Dojo plugin doesn't support derive macros on other items than struct and + // enum. + diagnostics.push_error( + "Dojo plugin doesn't support derive macros on other items than struct and enum." + .to_string(), + ); + } + } + + (rewrite_nodes, diagnostics) +} + +/// Extracts the names of the derive attributes from the given attributes. +/// +/// # Examples +/// +/// Derive usage should look like this: +/// +/// ```no_run,ignore +/// #[derive(Introspect)] +/// struct MyStruct {} +/// ``` +/// +/// And this function will return `["Introspect"]`. +pub fn extract_derive_attr_names( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + attrs: Vec, +) -> Vec { + attrs + .iter() + .filter_map(|attr| { + let args = attr.clone().structurize(db).args; + if args.is_empty() { + diagnostics.push_error("Expected args.".to_string()); + None + } else { + Some(args.into_iter().filter_map(|a| { + if let AttributeArgVariant::Unnamed(ast::Expr::Path(path)) = a.variant { + if let [ast::PathSegment::Simple(segment)] = &path.elements(db)[..] { + Some(segment.ident(db).text(db).to_string()) + } else { + None + } + } else { + None + } + })) + } + }) + .flatten() + .collect::>() +} + +/// Checks for conflicts between introspect and packed attributes. +/// +/// Introspect and IntrospectPacked cannot be used at a same time. +fn check_for_derive_attr_conflicts(diagnostics: &mut Vec, attr_names: &[String]) { + if attr_names.contains(&DOJO_INTROSPECT_DERIVE.to_string()) + && attr_names.contains(&DOJO_PACKED_DERIVE.to_string()) + { + diagnostics.push_error(format!( + "{} and {} attributes cannot be used at a same time.", + DOJO_INTROSPECT_DERIVE, DOJO_PACKED_DERIVE + )); + } +} diff --git a/crates/dojo/macros/src/derives/print.rs b/crates/dojo/macros/src/derives/print.rs new file mode 100644 index 0000000000..999adf2622 --- /dev/null +++ b/crates/dojo/macros/src/derives/print.rs @@ -0,0 +1,96 @@ +use cairo_lang_defs::patcher::RewriteNode; +use cairo_lang_syntax::node::ast::{ItemEnum, ItemStruct, OptionTypeClause}; +use cairo_lang_syntax::node::db::SyntaxGroup; +use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode}; +use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; + +/// Derives PrintTrait for a struct. +/// Parameters: +/// * db: The semantic database. +/// * struct_ast: The AST of the model struct. +/// +/// Returns: +/// * A RewriteNode containing the generated code. +pub fn handle_print_struct(db: &dyn SyntaxGroup, struct_ast: ItemStruct) -> RewriteNode { + let prints: Vec<_> = struct_ast + .members(db) + .elements(db) + .iter() + .map(|m| { + format!( + "core::debug::PrintTrait::print('{}'); core::debug::PrintTrait::print(self.{});", + m.name(db).text(db).to_string(), + m.name(db).text(db).to_string() + ) + }) + .collect(); + + RewriteNode::interpolate_patched( + " +#[cfg(test)] +impl $type_name$StructPrintImpl of core::debug::PrintTrait<$type_name$> { + fn print(self: $type_name$) { + $print$ + } +} +", + &UnorderedHashMap::from([ + ( + "type_name".to_string(), + RewriteNode::new_trimmed(struct_ast.name(db).as_syntax_node()), + ), + ("print".to_string(), RewriteNode::Text(prints.join("\n"))), + ]), + ) +} + +/// Derives PrintTrait for an enum. +/// Parameters: +/// * db: The semantic database. +/// * enum_ast: The AST of the model enum. +/// +/// Returns: +/// * A RewriteNode containing the generated code. +pub fn handle_print_enum(db: &dyn SyntaxGroup, enum_ast: ItemEnum) -> RewriteNode { + let enum_name = enum_ast.name(db).text(db); + let prints: Vec<_> = enum_ast + .variants(db) + .elements(db) + .iter() + .map(|m| { + let variant_name = m.name(db).text(db).to_string(); + match m.type_clause(db) { + OptionTypeClause::Empty(_) => { + format!( + "{enum_name}::{variant_name} => {{ \ + core::debug::PrintTrait::print('{variant_name}'); }}" + ) + } + OptionTypeClause::TypeClause(_) => { + format!( + "{enum_name}::{variant_name}(v) => {{ \ + core::debug::PrintTrait::print('{variant_name}'); \ + core::debug::PrintTrait::print(v); }}" + ) + } + } + }) + .collect(); + + RewriteNode::interpolate_patched( + " +#[cfg(test)] +impl $type_name$EnumPrintImpl of core::debug::PrintTrait<$type_name$> { + fn print(self: $type_name$) { + match self { + $print$ + } + } +} +", + &UnorderedHashMap::from([ + ("type_name".to_string(), RewriteNode::new_trimmed(enum_ast.name(db).as_syntax_node())), + ("print".to_string(), RewriteNode::Text(prints.join(",\n"))), + ]), + ) +} diff --git a/crates/dojo/macros/src/diagnostic_ext.rs b/crates/dojo/macros/src/diagnostic_ext.rs new file mode 100644 index 0000000000..05b4d0032e --- /dev/null +++ b/crates/dojo/macros/src/diagnostic_ext.rs @@ -0,0 +1,16 @@ +use cairo_lang_macro::Diagnostic; + +pub trait DiagnosticsExt { + fn push_error(&mut self, message: String); + fn push_warn(&mut self, message: String); +} + +impl DiagnosticsExt for Vec { + fn push_error(&mut self, message: String) { + self.push(Diagnostic::error(message)); + } + + fn push_warn(&mut self, message: String) { + self.push(Diagnostic::warn(message)); + } +} diff --git a/crates/dojo/macros/src/inlines/mod.rs b/crates/dojo/macros/src/inlines/mod.rs new file mode 100644 index 0000000000..341596eade --- /dev/null +++ b/crates/dojo/macros/src/inlines/mod.rs @@ -0,0 +1 @@ +pub mod selector_from_tag; diff --git a/crates/dojo/macros/src/inlines/selector_from_tag.rs b/crates/dojo/macros/src/inlines/selector_from_tag.rs new file mode 100644 index 0000000000..d032846805 --- /dev/null +++ b/crates/dojo/macros/src/inlines/selector_from_tag.rs @@ -0,0 +1,59 @@ +use cairo_lang_defs::patcher::PatchBuilder; +use cairo_lang_macro::{inline_macro, ProcMacroResult, TokenStream}; +use cairo_lang_parser::utils::SimpleParserDatabase; +use cairo_lang_syntax::node::kind::SyntaxKind::ItemInlineMacro; +use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; +use dojo_types::naming; + +use crate::proc_macro_result_ext::ProcMacroResultExt; + +#[inline_macro] +pub fn selector_from_tag(token_stream: TokenStream) -> ProcMacroResult { + handle_selector_from_tag_macro(token_stream) +} + +pub fn handle_selector_from_tag_macro(token_stream: TokenStream) -> ProcMacroResult { + let db = SimpleParserDatabase::default(); + let (root_node, _diagnostics) = db.parse_virtual_with_diagnostics(token_stream); + + for n in root_node.descendants(&db) { + if n.kind(&db) == ItemInlineMacro { + let node = ast::ItemInlineMacro::from_syntax_node(&db, n); + + let ast::WrappedArgList::ParenthesizedArgList(arg_list) = node.arguments(&db) else { + return ProcMacroResult::error( + "Macro `selector_from_tag!` does not support this bracket type.".to_string(), + ); + }; + + let args = arg_list.arguments(&db).elements(&db); + + if args.len() != 1 { + return ProcMacroResult::error( + "Invalid arguments. Expected \"selector_from_tag!(\"tag\")\"".to_string(), + ); + } + + let tag = &args[0].as_syntax_node().get_text(&db).replace('\"', ""); + + if !naming::is_valid_tag(tag) { + return ProcMacroResult::error( + "Invalid tag. Tag must be in the format of `namespace-name`.".to_string(), + ); + } + + let selector = naming::compute_selector_from_tag(tag); + + let mut builder = PatchBuilder::new(&db, &node); + builder.add_str(&format!("{:#64x}", selector)); + + let (code, _) = builder.build(); + + return ProcMacroResult::new(TokenStream::new(code)); + } + } + + ProcMacroResult::error( + "Macro `selector_from_tag!` must be called with a string parameter".to_string(), + ) +} diff --git a/crates/dojo/macros/src/lib.rs b/crates/dojo/macros/src/lib.rs new file mode 100644 index 0000000000..9225bc96b0 --- /dev/null +++ b/crates/dojo/macros/src/lib.rs @@ -0,0 +1,21 @@ +pub mod attributes; +pub mod derives; +pub mod diagnostic_ext; +pub mod inlines; +pub mod proc_macro_result_ext; + +#[cfg(test)] +pub mod tests; + +/// Prints the given string only if the `DOJO_EXPAND` environment variable is set. +/// This is useful for debugging the compiler with verbose output. +/// +/// # Arguments +/// +/// * `loc` - The location of the code to be expanded. +/// * `code` - The code to be expanded. +pub fn debug_expand(loc: &str, code: &str) { + if std::env::var("DOJO_EXPAND").is_ok() { + println!("\n// *> EXPAND {} <*\n{}\n\n", loc, code); + } +} diff --git a/crates/dojo/macros/src/proc_macro_result_ext.rs b/crates/dojo/macros/src/proc_macro_result_ext.rs new file mode 100644 index 0000000000..cd984a40af --- /dev/null +++ b/crates/dojo/macros/src/proc_macro_result_ext.rs @@ -0,0 +1,15 @@ +use cairo_lang_macro::{Diagnostics, ProcMacroResult, TokenStream}; + +use crate::diagnostic_ext::DiagnosticsExt; + +pub trait ProcMacroResultExt { + fn error(message: String) -> Self; +} + +impl ProcMacroResultExt for ProcMacroResult { + fn error(message: String) -> Self { + let mut diagnostics = vec![]; + diagnostics.push_error(message); + ProcMacroResult::new(TokenStream::empty()).with_diagnostics(Diagnostics::new(diagnostics)) + } +} diff --git a/crates/dojo/macros/src/tests/attributes/dojo_contract.rs b/crates/dojo/macros/src/tests/attributes/dojo_contract.rs new file mode 100644 index 0000000000..2ed97bdee9 --- /dev/null +++ b/crates/dojo/macros/src/tests/attributes/dojo_contract.rs @@ -0,0 +1,149 @@ +use cairo_lang_macro::TokenStream; + +use crate::attributes::constants::{DOJO_CONTRACT_ATTR, DOJO_MODEL_ATTR}; + +use crate::attributes::dojo_contract::{handle_module_attribute_macro, DOJO_INIT_FN}; +use crate::tests::utils::assert_output_stream; + +const SIMPLE_CONTRACT: &str = " +mod simple_contract { +} +"; + +const EXPANDED_SIMPLE_CONTRACT: &str = include_str!("./expanded/simple_contract.cairo"); + +const COMPLEX_CONTRACT: &str = " +mod complex_contract { + use starknet::{ContractAddress, get_caller_address}; + + #[derive(Copy, Drop, Serde)] + #[dojo::event] + struct MyInit { + #[key] + caller: ContractAddress, + value: u8, + } + + #[storage] + struct Storage { + value: u128 + } + + #[derive(Drop, starknet::Event)] + pub struct MyEvent { + #[key] + pub selector: felt252, + pub value: u64, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + MyEvent: MyEvent, + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.value.write(12); + } + + fn dojo_init(self: @ContractState, value: u8) { + let mut world = self.world(@\"ns\"); + world.emit_event(@MyInit { caller: get_caller_address(), value }); + } + + #[generate_trait] + impl SelfImpl of SelfTrait { + fn my_internal_function(self: @ContractState) -> u8 { + 42 + } + } +} +"; + +const EXPANDED_COMPLEX_CONTRACT: &str = include_str!("./expanded/complex_contract.cairo"); + +#[test] +fn test_contract_is_not_a_struct() { + let input = TokenStream::new("enum MyEnum { X, Y }".to_string()); + + let res = handle_module_attribute_macro(input); + + assert!(res.diagnostics.is_empty()); + assert!(res.token_stream.is_empty()); +} + +#[test] +fn test_contract_has_duplicated_attributes() { + let input = TokenStream::new(format!( + " + #[{DOJO_CONTRACT_ATTR}] + {SIMPLE_CONTRACT} + " + )); + + let res = handle_module_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + format!("Only one {DOJO_CONTRACT_ATTR} attribute is allowed per module.") + ); +} + +#[test] +fn test_contract_has_attribute_conflict() { + let input = TokenStream::new(format!( + " + #[{DOJO_MODEL_ATTR}] + {SIMPLE_CONTRACT} + " + )); + + let res = handle_module_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + format!("A {DOJO_CONTRACT_ATTR} can't be used together with a {DOJO_MODEL_ATTR}.") + ); +} + +#[test] +fn test_contract_has_bad_init_function() { + let input = TokenStream::new( + " +mod simple_contract { + fn dojo_init(self: @ContractState) -> u8 { + 0 + } +} + " + .to_string(), + ); + + let res = handle_module_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + format!("The {DOJO_INIT_FN} function cannot have a return type.") + ); +} + +#[test] +fn test_simple_contract() { + let input = TokenStream::new(SIMPLE_CONTRACT.to_string()); + + let res = handle_module_attribute_macro(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_SIMPLE_CONTRACT); +} + +#[test] +fn test_complex_contract() { + let input = TokenStream::new(COMPLEX_CONTRACT.to_string()); + + let res = handle_module_attribute_macro(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_COMPLEX_CONTRACT); +} diff --git a/crates/dojo/macros/src/tests/attributes/dojo_event.rs b/crates/dojo/macros/src/tests/attributes/dojo_event.rs new file mode 100644 index 0000000000..bce35a164d --- /dev/null +++ b/crates/dojo/macros/src/tests/attributes/dojo_event.rs @@ -0,0 +1,213 @@ +use cairo_lang_macro::TokenStream; + +use crate::attributes::constants::{DOJO_EVENT_ATTR, DOJO_MODEL_ATTR}; +use crate::attributes::dojo_event::handle_event_attribute_macro; +use crate::derives::DOJO_PACKED_DERIVE; +use crate::tests::utils::assert_output_stream; + +const SIMPLE_EVENT_WITHOUT_INTROSPECT: &str = " +#[derive(Drop, Serde)] +struct SimpleEvent { + #[key] + k: u32, + v: u32 +}"; + +const SIMPLE_EVENT: &str = " +#[derive(Introspect, Drop, Serde)] +struct SimpleEvent { + #[key] + k: u32, + v: u32 +}"; + +const EXPANDED_SIMPLE_EVENT: &str = include_str!("./expanded/simple_event.cairo"); + +const COMPLEX_EVENT: &str = " +#[derive(Introspect, Drop, Serde)] +struct ComplexEvent { + #[key] + k1: u8, + #[key] + k2: u32, + v1: u256, + v2: Option +}"; + +const EXPANDED_COMPLEX_EVENT: &str = include_str!("./expanded/complex_event.cairo"); + +#[test] +fn test_event_is_not_a_struct() { + let input = TokenStream::new("enum MyEnum { X, Y }".to_string()); + + let res = handle_event_attribute_macro(input); + + assert!(res.diagnostics.is_empty()); + assert!(res.token_stream.is_empty()); +} + +#[test] +fn test_event_has_duplicated_attributes() { + let input = TokenStream::new(format!( + " + #[{DOJO_EVENT_ATTR}] + {SIMPLE_EVENT} + " + )); + + let res = handle_event_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + format!("Only one {DOJO_EVENT_ATTR} attribute is allowed per module.") + ); +} + +#[test] +fn test_event_has_attribute_conflict() { + let input = TokenStream::new(format!( + " + #[{DOJO_MODEL_ATTR}] + {SIMPLE_EVENT} + " + )); + + let res = handle_event_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + format!("A {DOJO_EVENT_ATTR} can't be used together with a {DOJO_MODEL_ATTR}.") + ); +} + +#[test] +fn test_event_has_no_key() { + let input = TokenStream::new( + " + #[derive(Introspect, Drop, Serde)] + struct EventNoKey { + k: u32, + v: u32 + } + " + .to_string(), + ); + + let res = handle_event_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + "Event must define at least one #[key] attribute".to_string() + ); +} + +#[test] +fn test_event_has_no_value() { + let input = TokenStream::new( + " + #[derive(Introspect, Drop, Serde)] + struct EventNoValue { + #[key] + k: u32 + } + " + .to_string(), + ); + + let res = handle_event_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + "Event must define at least one member that is not a key".to_string() + ); +} + +#[test] +fn test_event_derives_from_introspect_packed() { + let input = TokenStream::new( + " + #[derive(IntrospectPacked, Drop, Serde)] + struct SimpleEvent { + #[key] + k: u32, + v: u32 + } + " + .to_string(), + ); + + let res = handle_event_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + format!("Deriving {DOJO_PACKED_DERIVE} on event is not allowed.") + ); +} + +#[test] +fn test_event_does_not_derive_from_drop() { + let input = TokenStream::new( + " + #[derive(Serde)] + struct SimpleModel { + #[key] + k: u32, + v: u32 + } + " + .to_string(), + ); + + let res = handle_event_attribute_macro(input); + + assert_eq!(res.diagnostics[0].message, "Event must derive from Drop and Serde.".to_string()); +} + +#[test] +fn test_event_does_not_derive_from_serde() { + let input = TokenStream::new( + " + #[derive(Drop)] + struct SimpleModel { + #[key] + k: u32, + v: u32 + } + " + .to_string(), + ); + + let res = handle_event_attribute_macro(input); + + assert_eq!(res.diagnostics[0].message, "Event must derive from Drop and Serde.".to_string()); +} + +#[test] +fn test_simple_event_without_introspect() { + let input = TokenStream::new(SIMPLE_EVENT_WITHOUT_INTROSPECT.to_string()); + + let res = handle_event_attribute_macro(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_SIMPLE_EVENT); +} + +#[test] +fn test_simple_event() { + let input = TokenStream::new(SIMPLE_EVENT.to_string()); + + let res = handle_event_attribute_macro(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_SIMPLE_EVENT); +} + +#[test] +fn test_complex_event() { + let input = TokenStream::new(COMPLEX_EVENT.to_string()); + + let res = handle_event_attribute_macro(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_COMPLEX_EVENT); +} diff --git a/crates/dojo/macros/src/tests/attributes/dojo_model.rs b/crates/dojo/macros/src/tests/attributes/dojo_model.rs new file mode 100644 index 0000000000..d5acac0711 --- /dev/null +++ b/crates/dojo/macros/src/tests/attributes/dojo_model.rs @@ -0,0 +1,212 @@ +use cairo_lang_macro::TokenStream; + +use crate::attributes::constants::{DOJO_EVENT_ATTR, DOJO_MODEL_ATTR}; +use crate::attributes::dojo_model::handle_model_attribute_macro; +use crate::tests::utils::assert_output_stream; + +const SIMPLE_MODEL: &str = " +#[derive(Introspect, Drop, Serde)] +struct SimpleModel { + #[key] + k: u32, + v: u32 +}"; + +const SIMPLE_MODEL_WITHOUT_INTROSPECT: &str = " +#[derive(Drop, Serde)] +struct SimpleModel { + #[key] + k: u32, + v: u32 +}"; + +const EXPANDED_SIMPLE_MODEL: &str = include_str!("./expanded/simple_model.cairo"); + +const COMPLEX_MODEL: &str = " +#[derive(Introspect, Drop, Serde)] +struct ComplexModel { + #[key] + k1: u8, + #[key] + k2: u32, + v1: u256, + v2: Option +}"; + +const EXPANDED_COMPLEX_MODEL: &str = include_str!("./expanded/complex_model.cairo"); + +#[test] +fn test_model_is_not_a_struct() { + let input = TokenStream::new("enum MyEnum { X, Y }".to_string()); + + let res = handle_model_attribute_macro(input); + + assert!(res.diagnostics.is_empty()); + assert!(res.token_stream.is_empty()); +} + +#[test] +fn test_model_has_duplicated_attributes() { + let input = TokenStream::new(format!( + " + #[{DOJO_MODEL_ATTR}] + {SIMPLE_MODEL} + " + )); + + let res = handle_model_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + format!("Only one {DOJO_MODEL_ATTR} attribute is allowed per module.") + ); +} + +#[test] +fn test_model_has_attribute_conflict() { + let input = TokenStream::new(format!( + " + #[{DOJO_EVENT_ATTR}] + {SIMPLE_MODEL} + " + )); + + let res = handle_model_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + format!("A {DOJO_MODEL_ATTR} can't be used together with a {DOJO_EVENT_ATTR}.") + ); +} + +#[test] +fn test_model_has_no_key() { + let input = TokenStream::new( + " + #[derive(Introspect, Drop, Serde)] + struct ModelNoKey { + k: u32, + v: u32 + } + " + .to_string(), + ); + + let res = handle_model_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + "Model must define at least one #[key] attribute".to_string() + ); +} + +#[test] +fn test_model_has_no_value() { + let input = TokenStream::new( + " + #[derive(Introspect, Drop, Serde)] + struct ModelNoValue { + #[key] + k: u32 + } + " + .to_string(), + ); + + let res = handle_model_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + "Model must define at least one member that is not a key".to_string() + ); +} + +#[test] +fn test_model_derives_from_both_introspect_and_packed() { + let input = TokenStream::new( + " + #[derive(Introspect, IntrospectPacked, Drop, Serde)] + struct SimpleModel { + #[key] + k: u32, + v: u32 + } + " + .to_string(), + ); + + let res = handle_model_attribute_macro(input); + + assert_eq!( + res.diagnostics[0].message, + "Model cannot derive from both Introspect and IntrospectPacked.".to_string() + ); +} + +#[test] +fn test_model_does_not_derive_from_drop() { + let input = TokenStream::new( + " + #[derive(Serde)] + struct SimpleModel { + #[key] + k: u32, + v: u32 + } + " + .to_string(), + ); + + let res = handle_model_attribute_macro(input); + + assert_eq!(res.diagnostics[0].message, "Model must derive from Drop and Serde.".to_string()); +} + +#[test] +fn test_model_does_not_derive_from_serde() { + let input = TokenStream::new( + " + #[derive(Drop)] + struct SimpleModel { + #[key] + k: u32, + v: u32 + } + " + .to_string(), + ); + + let res = handle_model_attribute_macro(input); + + assert_eq!(res.diagnostics[0].message, "Model must derive from Drop and Serde.".to_string()); +} + +#[test] +fn test_simple_model() { + let input = TokenStream::new(SIMPLE_MODEL.to_string()); + + let res = handle_model_attribute_macro(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_SIMPLE_MODEL); +} + +#[test] +fn test_simple_model_without_introspect() { + let input = TokenStream::new(SIMPLE_MODEL_WITHOUT_INTROSPECT.to_string()); + + let res = handle_model_attribute_macro(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_SIMPLE_MODEL); +} + +#[test] +fn test_complex_model() { + let input = TokenStream::new(COMPLEX_MODEL.to_string()); + + let res = handle_model_attribute_macro(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_COMPLEX_MODEL); +} diff --git a/crates/dojo/macros/src/tests/attributes/expanded/complex_contract.cairo b/crates/dojo/macros/src/tests/attributes/expanded/complex_contract.cairo new file mode 100644 index 0000000000..a301a7f661 --- /dev/null +++ b/crates/dojo/macros/src/tests/attributes/expanded/complex_contract.cairo @@ -0,0 +1,106 @@ +#[starknet::contract] +pub mod complex_contract { + use dojo::contract::components::world_provider::{ + world_provider_cpt, world_provider_cpt::InternalTrait as WorldProviderInternal, + IWorldProvider + }; + use dojo::contract::components::upgradeable::upgradeable_cpt; + use dojo::contract::IContract; + use dojo::meta::IDeployedResource; + + component!(path: world_provider_cpt, storage: world_provider, event: WorldProviderEvent); + component!(path: upgradeable_cpt, storage: upgradeable, event: UpgradeableEvent); + + #[abi(embed_v0)] + impl WorldProviderImpl = world_provider_cpt::WorldProviderImpl; + + #[abi(embed_v0)] + impl UpgradeableImpl = upgradeable_cpt::UpgradeableImpl; + + #[abi(embed_v0)] + pub impl complex_contract__ContractImpl of IContract {} + + #[abi(embed_v0)] + pub impl complex_contract__DeployedContractImpl of IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "complex_contract" + } + } + + #[generate_trait] + impl complex_contractInternalImpl of complex_contractInternalTrait { + fn world( + self: @ContractState, namespace: @ByteArray + ) -> dojo::world::storage::WorldStorage { + dojo::world::WorldStorageTrait::new(self.world_provider.world_dispatcher(), namespace) + } + } + + use starknet::{ContractAddress, get_caller_address}; + + #[derive(Copy, Drop, Serde)] + #[dojo::event] + struct MyInit { + #[key] + caller: ContractAddress, + value: u8, + } + + #[storage] + struct Storage { + #[substorage(v0)] + upgradeable: upgradeable_cpt::Storage, + #[substorage(v0)] + world_provider: world_provider_cpt::Storage, + value: u128 + } + + #[derive(Drop, starknet::Event)] + pub struct MyEvent { + #[key] + pub selector: felt252, + pub value: u64, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: upgradeable_cpt::Event, + WorldProviderEvent: world_provider_cpt::Event, + MyEvent: MyEvent + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.world_provider.initializer(); + self.value.write(12); + } + + #[abi(per_item)] + #[generate_trait] + pub impl IDojoInitImpl of IDojoInit { + #[external(v0)] + fn dojo_init(self: @ContractState, value: u8) { + if starknet::get_caller_address() != self + .world_provider + .world_dispatcher() + .contract_address { + core::panics::panic_with_byte_array( + @format!( + "Only the world can init contract `{}`, but caller is `{:?}`", + self.dojo_name(), + starknet::get_caller_address() + ) + ); + } + let mut world = self.world(@"ns"); + world.emit_event(@MyInit { caller: get_caller_address(), value }); + } + } + #[generate_trait] + impl SelfImpl of SelfTrait { + fn my_internal_function(self: @ContractState) -> u8 { + 42 + } + } +} diff --git a/crates/dojo/macros/src/tests/attributes/expanded/complex_event.cairo b/crates/dojo/macros/src/tests/attributes/expanded/complex_event.cairo new file mode 100644 index 0000000000..429888c065 --- /dev/null +++ b/crates/dojo/macros/src/tests/attributes/expanded/complex_event.cairo @@ -0,0 +1,89 @@ +#[derive(Introspect)] +struct ComplexEvent { + #[key] + k1: u8, + #[key] + k2: u32, + v1: u256, + v2: Option +} + +// EventValue on it's own does nothing since events are always emitted and +// never read from the storage. However, it's required by the ABI to +// ensure that the event definition contains both keys and values easily distinguishable. +// Only derives strictly required traits. +#[derive(Drop, Serde)] +pub struct ComplexEventValue { + pub v1: u256, + pub v2: Option, +} + +pub impl ComplexEventDefinition of dojo::event::EventDefinition { + #[inline(always)] + fn name() -> ByteArray { + "ComplexEvent" + } +} + +pub impl ComplexEventModelParser of dojo::model::model::ModelParser { + fn serialize_keys(self: @ComplexEvent) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + core::serde::Serde::serialize(self.k1, ref serialized); + core::serde::Serde::serialize(self.k2, ref serialized); + + core::array::ArrayTrait::span(@serialized) + } + fn serialize_values(self: @ComplexEvent) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + core::serde::Serde::serialize(self.v1, ref serialized); + core::serde::Serde::serialize(self.v2, ref serialized); + + core::array::ArrayTrait::span(@serialized) + } +} + +pub impl ComplexEventEventImpl = dojo::event::event::EventImpl; + +#[starknet::contract] +pub mod e_ComplexEvent { + use super::ComplexEvent; + use super::ComplexEventValue; + + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl ComplexEvent__DeployedEventImpl = + dojo::event::component::IDeployedEventImpl; + + #[abi(embed_v0)] + impl ComplexEvent__StoredEventImpl = + dojo::event::component::IStoredEventImpl; + + #[abi(embed_v0)] + impl ComplexEvent__EventImpl = + dojo::event::component::IEventImpl; + + #[abi(per_item)] + #[generate_trait] + impl ComplexEventImpl of IComplexEvent { + // Ensures the ABI contains the Event struct, since it's never used + // by systems directly. + #[external(v0)] + fn ensure_abi(self: @ContractState, event: ComplexEvent) { + let _event = event; + } + + // Outputs EventValue to allow a simple diff from the ABI compared to the + // event to retrieved the keys of an event. + #[external(v0)] + fn ensure_values(self: @ContractState, value: ComplexEventValue) { + let _value = value; + } + + // Ensures the generated contract has a unique classhash, using + // a hardcoded hash computed on event and member names. + #[external(v0)] + fn ensure_unique(self: @ContractState) {} + } +} diff --git a/crates/dojo/macros/src/tests/attributes/expanded/complex_model.cairo b/crates/dojo/macros/src/tests/attributes/expanded/complex_model.cairo new file mode 100644 index 0000000000..2b9daeb044 --- /dev/null +++ b/crates/dojo/macros/src/tests/attributes/expanded/complex_model.cairo @@ -0,0 +1,140 @@ +#[derive(Introspect)] +struct ComplexModel { + #[key] + k1: u8, + #[key] + k2: u32, + v1: u256, + v2: Option +} + +#[derive(Drop, Serde)] +pub struct ComplexModelValue { + pub v1: u256, + pub v2: Option, +} + +type ComplexModelKeyType = (u8, u32); + +pub impl ComplexModelKeyParser of dojo::model::model::KeyParser { + #[inline(always)] + fn parse_key(self: @ComplexModel) -> ComplexModelKeyType { + (*self.k1, *self.k2) + } +} + +impl ComplexModelModelValueKey of dojo::model::model_value::ModelValueKey< + ComplexModelValue, ComplexModelKeyType +> {} + +// Impl to get the static definition of a model +pub mod m_ComplexModel_definition { + use super::ComplexModel; + pub impl ComplexModelDefinitionImpl of dojo::model::ModelDefinition { + #[inline(always)] + fn name() -> ByteArray { + "ComplexModel" + } + + #[inline(always)] + fn layout() -> dojo::meta::Layout { + dojo::meta::Introspect::::layout() + } + + #[inline(always)] + fn schema() -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(s) = + dojo::meta::Introspect::::ty() { + s + } else { + panic!("Model `ComplexModel`: invalid schema.") + } + } + + #[inline(always)] + fn size() -> Option { + dojo::meta::Introspect::::size() + } + } +} + +pub impl ComplexModelDefinition = + m_ComplexModel_definition::ComplexModelDefinitionImpl; +pub impl ComplexModelModelValueDefinition = + m_ComplexModel_definition::ComplexModelDefinitionImpl; + +pub impl ComplexModelModelParser of dojo::model::model::ModelParser { + fn serialize_keys(self: @ComplexModel) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + core::serde::Serde::serialize(self.k1, ref serialized); + core::serde::Serde::serialize(self.k2, ref serialized); + + core::array::ArrayTrait::span(@serialized) + } + fn serialize_values(self: @ComplexModel) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + core::serde::Serde::serialize(self.v1, ref serialized); + core::serde::Serde::serialize(self.v2, ref serialized); + + core::array::ArrayTrait::span(@serialized) + } +} + +pub impl ComplexModelModelValueParser of dojo::model::model_value::ModelValueParser< + ComplexModelValue +> { + fn serialize_values(self: @ComplexModelValue) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + core::serde::Serde::serialize(self.v1, ref serialized); + core::serde::Serde::serialize(self.v2, ref serialized); + + core::array::ArrayTrait::span(@serialized) + } +} + +pub impl ComplexModelModelImpl = dojo::model::model::ModelImpl; +pub impl ComplexModelModelValueImpl = dojo::model::model_value::ModelValueImpl; + +#[starknet::contract] +pub mod m_ComplexModel { + use super::ComplexModel; + use super::ComplexModelValue; + + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl ComplexModel__DojoDeployedModelImpl = + dojo::model::component::IDeployedModelImpl; + + #[abi(embed_v0)] + impl ComplexModel__DojoStoredModelImpl = + dojo::model::component::IStoredModelImpl; + + #[abi(embed_v0)] + impl ComplexModel__DojoModelImpl = + dojo::model::component::IModelImpl; + + #[abi(per_item)] + #[generate_trait] + impl ComplexModelImpl of IComplexModel { + // Ensures the ABI contains the Model struct, even if never used + // into as a system input. + #[external(v0)] + fn ensure_abi(self: @ContractState, model: ComplexModel) { + let _model = model; + } + + // Outputs ModelValue to allow a simple diff from the ABI compared to the + // model to retrieved the keys of a model. + #[external(v0)] + fn ensure_values(self: @ContractState, value: ComplexModelValue) { + let _value = value; + } + + // Ensures the generated contract has a unique classhash, using + // a hardcoded hash computed on model and member names. + #[external(v0)] + fn ensure_unique(self: @ContractState) {} + } +} diff --git a/crates/dojo/macros/src/tests/attributes/expanded/simple_contract.cairo b/crates/dojo/macros/src/tests/attributes/expanded/simple_contract.cairo new file mode 100644 index 0000000000..848ac02431 --- /dev/null +++ b/crates/dojo/macros/src/tests/attributes/expanded/simple_contract.cairo @@ -0,0 +1,78 @@ +#[starknet::contract] +pub mod simple_contract { + use dojo::contract::components::world_provider::{ + world_provider_cpt, world_provider_cpt::InternalTrait as WorldProviderInternal, + IWorldProvider + }; + use dojo::contract::components::upgradeable::upgradeable_cpt; + use dojo::contract::IContract; + use dojo::meta::IDeployedResource; + + component!(path: world_provider_cpt, storage: world_provider, event: WorldProviderEvent); + component!(path: upgradeable_cpt, storage: upgradeable, event: UpgradeableEvent); + + #[abi(embed_v0)] + impl WorldProviderImpl = world_provider_cpt::WorldProviderImpl; + + #[abi(embed_v0)] + impl UpgradeableImpl = upgradeable_cpt::UpgradeableImpl; + + #[abi(embed_v0)] + pub impl simple_contract__ContractImpl of IContract {} + + #[abi(embed_v0)] + pub impl simple_contract__DeployedContractImpl of IDeployedResource { + fn dojo_name(self: @ContractState) -> ByteArray { + "simple_contract" + } + } + + #[generate_trait] + impl simple_contractInternalImpl of simple_contractInternalTrait { + fn world( + self: @ContractState, namespace: @ByteArray + ) -> dojo::world::storage::WorldStorage { + dojo::world::WorldStorageTrait::new(self.world_provider.world_dispatcher(), namespace) + } + } + + + #[constructor] + fn constructor(ref self: ContractState) { + self.world_provider.initializer(); + } + #[abi(per_item)] + #[generate_trait] + pub impl IDojoInitImpl of IDojoInit { + #[external(v0)] + fn dojo_init(self: @ContractState) { + if starknet::get_caller_address() != self + .world_provider + .world_dispatcher() + .contract_address { + core::panics::panic_with_byte_array( + @format!( + "Only the world can init contract `{}`, but caller is `{:?}`", + self.dojo_name(), + starknet::get_caller_address(), + ) + ); + } + } + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + UpgradeableEvent: upgradeable_cpt::Event, + WorldProviderEvent: world_provider_cpt::Event, + } + + #[storage] + struct Storage { + #[substorage(v0)] + upgradeable: upgradeable_cpt::Storage, + #[substorage(v0)] + world_provider: world_provider_cpt::Storage, + } +} diff --git a/crates/dojo/macros/src/tests/attributes/expanded/simple_event.cairo b/crates/dojo/macros/src/tests/attributes/expanded/simple_event.cairo new file mode 100644 index 0000000000..caa83a6e87 --- /dev/null +++ b/crates/dojo/macros/src/tests/attributes/expanded/simple_event.cairo @@ -0,0 +1,83 @@ +#[derive(Introspect)] +struct SimpleEvent { + #[key] + k: u32, + v: u32 +} + +// EventValue on it's own does nothing since events are always emitted and +// never read from the storage. However, it's required by the ABI to +// ensure that the event definition contains both keys and values easily distinguishable. +// Only derives strictly required traits. +#[derive(Drop, Serde)] +pub struct SimpleEventValue { + pub v: u32, +} + +pub impl SimpleEventDefinition of dojo::event::EventDefinition { + #[inline(always)] + fn name() -> ByteArray { + "SimpleEvent" + } +} + +pub impl SimpleEventModelParser of dojo::model::model::ModelParser { + fn serialize_keys(self: @SimpleEvent) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + core::serde::Serde::serialize(self.k, ref serialized); + + core::array::ArrayTrait::span(@serialized) + } + fn serialize_values(self: @SimpleEvent) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + core::serde::Serde::serialize(self.v, ref serialized); + + core::array::ArrayTrait::span(@serialized) + } +} + +pub impl SimpleEventEventImpl = dojo::event::event::EventImpl; + +#[starknet::contract] +pub mod e_SimpleEvent { + use super::SimpleEvent; + use super::SimpleEventValue; + + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl SimpleEvent__DeployedEventImpl = + dojo::event::component::IDeployedEventImpl; + + #[abi(embed_v0)] + impl SimpleEvent__StoredEventImpl = + dojo::event::component::IStoredEventImpl; + + #[abi(embed_v0)] + impl SimpleEvent__EventImpl = + dojo::event::component::IEventImpl; + + #[abi(per_item)] + #[generate_trait] + impl SimpleEventImpl of ISimpleEvent { + // Ensures the ABI contains the Event struct, since it's never used + // by systems directly. + #[external(v0)] + fn ensure_abi(self: @ContractState, event: SimpleEvent) { + let _event = event; + } + + // Outputs EventValue to allow a simple diff from the ABI compared to the + // event to retrieved the keys of an event. + #[external(v0)] + fn ensure_values(self: @ContractState, value: SimpleEventValue) { + let _value = value; + } + + // Ensures the generated contract has a unique classhash, using + // a hardcoded hash computed on event and member names. + #[external(v0)] + fn ensure_unique(self: @ContractState) {} + } +} diff --git a/crates/dojo/macros/src/tests/attributes/expanded/simple_model.cairo b/crates/dojo/macros/src/tests/attributes/expanded/simple_model.cairo new file mode 100644 index 0000000000..36323ca14b --- /dev/null +++ b/crates/dojo/macros/src/tests/attributes/expanded/simple_model.cairo @@ -0,0 +1,132 @@ +#[derive(Introspect)] +struct SimpleModel { + #[key] + k: u32, + v: u32 +} + +#[derive(Drop, Serde)] +pub struct SimpleModelValue { + pub v: u32, +} + +type SimpleModelKeyType = u32; + +pub impl SimpleModelKeyParser of dojo::model::model::KeyParser { + #[inline(always)] + fn parse_key(self: @SimpleModel) -> SimpleModelKeyType { + *self.k + } +} + +impl SimpleModelModelValueKey of dojo::model::model_value::ModelValueKey< + SimpleModelValue, SimpleModelKeyType +> {} + +// Impl to get the static definition of a model +pub mod m_SimpleModel_definition { + use super::SimpleModel; + pub impl SimpleModelDefinitionImpl of dojo::model::ModelDefinition { + #[inline(always)] + fn name() -> ByteArray { + "SimpleModel" + } + + #[inline(always)] + fn layout() -> dojo::meta::Layout { + dojo::meta::Introspect::::layout() + } + + #[inline(always)] + fn schema() -> dojo::meta::introspect::Struct { + if let dojo::meta::introspect::Ty::Struct(s) = + dojo::meta::Introspect::::ty() { + s + } else { + panic!("Model `SimpleModel`: invalid schema.") + } + } + + #[inline(always)] + fn size() -> Option { + dojo::meta::Introspect::::size() + } + } +} + +pub impl SimpleModelDefinition = m_SimpleModel_definition::SimpleModelDefinitionImpl; +pub impl SimpleModelModelValueDefinition = + m_SimpleModel_definition::SimpleModelDefinitionImpl; + +pub impl SimpleModelModelParser of dojo::model::model::ModelParser { + fn serialize_keys(self: @SimpleModel) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + core::serde::Serde::serialize(self.k, ref serialized); + + core::array::ArrayTrait::span(@serialized) + } + fn serialize_values(self: @SimpleModel) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + core::serde::Serde::serialize(self.v, ref serialized); + + core::array::ArrayTrait::span(@serialized) + } +} + +pub impl SimpleModelModelValueParser of dojo::model::model_value::ModelValueParser< + SimpleModelValue +> { + fn serialize_values(self: @SimpleModelValue) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + core::serde::Serde::serialize(self.v, ref serialized); + + core::array::ArrayTrait::span(@serialized) + } +} + +pub impl SimpleModelModelImpl = dojo::model::model::ModelImpl; +pub impl SimpleModelModelValueImpl = dojo::model::model_value::ModelValueImpl; + +#[starknet::contract] +pub mod m_SimpleModel { + use super::SimpleModel; + use super::SimpleModelValue; + + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl SimpleModel__DojoDeployedModelImpl = + dojo::model::component::IDeployedModelImpl; + + #[abi(embed_v0)] + impl SimpleModel__DojoStoredModelImpl = + dojo::model::component::IStoredModelImpl; + + #[abi(embed_v0)] + impl SimpleModel__DojoModelImpl = + dojo::model::component::IModelImpl; + + #[abi(per_item)] + #[generate_trait] + impl SimpleModelImpl of ISimpleModel { + // Ensures the ABI contains the Model struct, even if never used + // into as a system input. + #[external(v0)] + fn ensure_abi(self: @ContractState, model: SimpleModel) { + let _model = model; + } + + // Outputs ModelValue to allow a simple diff from the ABI compared to the + // model to retrieved the keys of a model. + #[external(v0)] + fn ensure_values(self: @ContractState, value: SimpleModelValue) { + let _value = value; + } + + // Ensures the generated contract has a unique classhash, using + // a hardcoded hash computed on model and member names. + #[external(v0)] + fn ensure_unique(self: @ContractState) {} + } +} diff --git a/crates/dojo/macros/src/tests/derives/expanded/complex_enum.cairo b/crates/dojo/macros/src/tests/derives/expanded/complex_enum.cairo new file mode 100644 index 0000000000..ba61d4fa31 --- /dev/null +++ b/crates/dojo/macros/src/tests/derives/expanded/complex_enum.cairo @@ -0,0 +1,57 @@ +impl ComplexEnumIntrospect<> of dojo::meta::introspect::Introspect> { + #[inline(always)] + fn size() -> Option { + Option::None + } + + fn layout() -> dojo::meta::Layout { + dojo::meta::Layout::Enum( + array![ + dojo::meta::FieldLayout { + selector: 0, layout: dojo::meta::introspect::Introspect::::layout() + }, + dojo::meta::FieldLayout { + selector: 1, layout: dojo::meta::introspect::Introspect::>::layout() + }, + dojo::meta::FieldLayout { + selector: 2, + layout: dojo::meta::Layout::Tuple( + array![ + dojo::meta::introspect::Introspect::::layout(), + dojo::meta::introspect::Introspect::::layout(), + dojo::meta::introspect::Introspect::::layout() + ] + .span() + ) + } + ] + .span() + ) + } + + #[inline(always)] + fn ty() -> dojo::meta::introspect::Ty { + dojo::meta::introspect::Ty::Enum( + dojo::meta::introspect::Enum { + name: 'ComplexEnum', + attrs: array![].span(), + children: array![ + ('VARIANT1', dojo::meta::introspect::Introspect::::ty()), + ('VARIANT2', dojo::meta::introspect::Introspect::>::ty()), + ( + 'VARIANT3', + dojo::meta::introspect::Ty::Tuple( + array![ + dojo::meta::introspect::Introspect::::ty(), + dojo::meta::introspect::Introspect::::ty(), + dojo::meta::introspect::Introspect::::ty() + ] + .span() + ) + ) + ] + .span() + } + ) + } +} diff --git a/crates/dojo/macros/src/tests/derives/expanded/complex_struct.cairo b/crates/dojo/macros/src/tests/derives/expanded/complex_struct.cairo new file mode 100644 index 0000000000..e3d04e9520 --- /dev/null +++ b/crates/dojo/macros/src/tests/derives/expanded/complex_struct.cairo @@ -0,0 +1,82 @@ +impl ComplexStructIntrospect<> of dojo::meta::introspect::Introspect> { + #[inline(always)] + fn size() -> Option { + Option::None + } + + fn layout() -> dojo::meta::Layout { + dojo::meta::Layout::Struct( + array![ + dojo::meta::FieldLayout { + selector: 687013198911006804117413256380548377255056948723479227932116677690621743639, + layout: dojo::meta::introspect::Introspect::>::layout() + }, + dojo::meta::FieldLayout { + selector: 573200779692275582020388969134054872186051594998702457223229675092771367647, + layout: dojo::meta::introspect::Introspect::>::layout() + }, + dojo::meta::FieldLayout { + selector: 268067745408767739723108330020913373797853558774636706294407751171317330906, + layout: dojo::meta::Layout::Tuple( + array![ + dojo::meta::introspect::Introspect::>::layout(), + dojo::meta::introspect::Introspect::::layout(), + dojo::meta::introspect::Introspect::>::layout() + ] + .span() + ) + } + ] + .span() + ) + } + + #[inline(always)] + fn ty() -> dojo::meta::introspect::Ty { + dojo::meta::introspect::Ty::Struct( + dojo::meta::introspect::Struct { + name: 'ComplexStruct', + attrs: array![].span(), + children: array![ + dojo::meta::introspect::Member { + name: 'k1', + attrs: array!['key'].span(), + ty: dojo::meta::introspect::Introspect::::ty() + }, + dojo::meta::introspect::Member { + name: 'k2', + attrs: array!['key'].span(), + ty: dojo::meta::introspect::Introspect::::ty() + }, + dojo::meta::introspect::Member { + name: 'v1', + attrs: array![].span(), + ty: dojo::meta::introspect::Ty::Array( + array![dojo::meta::introspect::Introspect::::ty()].span() + ) + }, + dojo::meta::introspect::Member { + name: 'v2', + attrs: array![].span(), + ty: dojo::meta::introspect::Introspect::>::ty() + }, + dojo::meta::introspect::Member { + name: 'v3', + attrs: array![].span(), + ty: dojo::meta::introspect::Ty::Tuple( + array![ + dojo::meta::introspect::Ty::Array( + array![dojo::meta::introspect::Introspect::::ty()].span() + ), + dojo::meta::introspect::Introspect::::ty(), + dojo::meta::introspect::Introspect::>::ty() + ] + .span() + ) + } + ] + .span() + } + ) + } +} diff --git a/crates/dojo/macros/src/tests/derives/expanded/packed_enum.cairo b/crates/dojo/macros/src/tests/derives/expanded/packed_enum.cairo new file mode 100644 index 0000000000..7a993979c2 --- /dev/null +++ b/crates/dojo/macros/src/tests/derives/expanded/packed_enum.cairo @@ -0,0 +1,87 @@ +impl PackedEnumIntrospect<> of dojo::meta::introspect::Introspect> { + #[inline(always)] + fn size() -> Option { + Option::Some(3) + } + + fn layout() -> dojo::meta::Layout { + dojo::meta::Layout::Enum( + array![ + dojo::meta::FieldLayout { + selector: 0, + layout: dojo::meta::Layout::Tuple( + array![ + dojo::meta::introspect::Introspect::::layout(), + dojo::meta::introspect::Introspect::::layout() + ] + .span() + ) + }, + dojo::meta::FieldLayout { + selector: 1, + layout: dojo::meta::Layout::Tuple( + array![ + dojo::meta::introspect::Introspect::::layout(), + dojo::meta::introspect::Introspect::::layout() + ] + .span() + ) + }, + dojo::meta::FieldLayout { + selector: 2, + layout: dojo::meta::Layout::Tuple( + array![ + dojo::meta::introspect::Introspect::::layout(), + dojo::meta::introspect::Introspect::::layout() + ] + .span() + ) + } + ] + .span() + ) + } + + #[inline(always)] + fn ty() -> dojo::meta::introspect::Ty { + dojo::meta::introspect::Ty::Enum( + dojo::meta::introspect::Enum { + name: 'PackedEnum', + attrs: array![].span(), + children: array![ + ( + 'VARIANT1', + dojo::meta::introspect::Ty::Tuple( + array![ + dojo::meta::introspect::Introspect::::ty(), + dojo::meta::introspect::Introspect::::ty() + ] + .span() + ) + ), + ( + 'VARIANT2', + dojo::meta::introspect::Ty::Tuple( + array![ + dojo::meta::introspect::Introspect::::ty(), + dojo::meta::introspect::Introspect::::ty() + ] + .span() + ) + ), + ( + 'VARIANT3', + dojo::meta::introspect::Ty::Tuple( + array![ + dojo::meta::introspect::Introspect::::ty(), + dojo::meta::introspect::Introspect::::ty() + ] + .span() + ) + ) + ] + .span() + } + ) + } +} diff --git a/crates/dojo/macros/src/tests/derives/expanded/packed_struct.cairo b/crates/dojo/macros/src/tests/derives/expanded/packed_struct.cairo new file mode 100644 index 0000000000..dc305e79df --- /dev/null +++ b/crates/dojo/macros/src/tests/derives/expanded/packed_struct.cairo @@ -0,0 +1,44 @@ +impl SimpleStructIntrospect<> of dojo::meta::introspect::Introspect> { + #[inline(always)] + fn size() -> Option { + Option::Some(3) + } + + fn layout() -> dojo::meta::Layout { + dojo::meta::Layout::Fixed(array![32, 8, 16].span()) + } + + #[inline(always)] + fn ty() -> dojo::meta::introspect::Ty { + dojo::meta::introspect::Ty::Struct( + dojo::meta::introspect::Struct { + name: 'SimpleStruct', + attrs: array![].span(), + children: array![ + dojo::meta::introspect::Member { + name: 'k1', + attrs: array!['key'].span(), + ty: dojo::meta::introspect::Introspect::::ty() + }, + dojo::meta::introspect::Member { + name: 'v1', + attrs: array![].span(), + ty: dojo::meta::introspect::Introspect::::ty() + }, + dojo::meta::introspect::Member { + name: 'v2', + attrs: array![].span(), + ty: dojo::meta::introspect::Ty::Tuple( + array![ + dojo::meta::introspect::Introspect::::ty(), + dojo::meta::introspect::Introspect::::ty() + ] + .span() + ) + } + ] + .span() + } + ) + } +} diff --git a/crates/dojo/macros/src/tests/derives/expanded/simple_enum.cairo b/crates/dojo/macros/src/tests/derives/expanded/simple_enum.cairo new file mode 100644 index 0000000000..aee76638fa --- /dev/null +++ b/crates/dojo/macros/src/tests/derives/expanded/simple_enum.cairo @@ -0,0 +1,39 @@ +impl SimpleEnumIntrospect<> of dojo::meta::introspect::Introspect> { + #[inline(always)] + fn size() -> Option { + Option::Some(1) + } + + fn layout() -> dojo::meta::Layout { + dojo::meta::Layout::Enum( + array![ + dojo::meta::FieldLayout { + selector: 0, layout: dojo::meta::Layout::Fixed(array![].span()) + }, + dojo::meta::FieldLayout { + selector: 1, layout: dojo::meta::Layout::Fixed(array![].span()) + }, + dojo::meta::FieldLayout { + selector: 2, layout: dojo::meta::Layout::Fixed(array![].span()) + } + ] + .span() + ) + } + + #[inline(always)] + fn ty() -> dojo::meta::introspect::Ty { + dojo::meta::introspect::Ty::Enum( + dojo::meta::introspect::Enum { + name: 'SimpleEnum', + attrs: array![].span(), + children: array![ + ('VARIANT1', dojo::meta::introspect::Ty::Tuple(array![].span())), + ('VARIANT2', dojo::meta::introspect::Ty::Tuple(array![].span())), + ('VARIANT3', dojo::meta::introspect::Ty::Tuple(array![].span())) + ] + .span() + } + ) + } +} diff --git a/crates/dojo/macros/src/tests/derives/expanded/simple_struct.cairo b/crates/dojo/macros/src/tests/derives/expanded/simple_struct.cairo new file mode 100644 index 0000000000..9a797e8b20 --- /dev/null +++ b/crates/dojo/macros/src/tests/derives/expanded/simple_struct.cairo @@ -0,0 +1,62 @@ +impl SimpleStructIntrospect<> of dojo::meta::introspect::Introspect> { + #[inline(always)] + fn size() -> Option { + Option::Some(3) + } + + fn layout() -> dojo::meta::Layout { + dojo::meta::Layout::Struct( + array![ + dojo::meta::FieldLayout { + selector: 687013198911006804117413256380548377255056948723479227932116677690621743639, + layout: dojo::meta::introspect::Introspect::::layout() + }, + dojo::meta::FieldLayout { + selector: 573200779692275582020388969134054872186051594998702457223229675092771367647, + layout: dojo::meta::Layout::Tuple( + array![ + dojo::meta::introspect::Introspect::::layout(), + dojo::meta::introspect::Introspect::::layout() + ] + .span() + ) + } + ] + .span() + ) + } + + #[inline(always)] + fn ty() -> dojo::meta::introspect::Ty { + dojo::meta::introspect::Ty::Struct( + dojo::meta::introspect::Struct { + name: 'SimpleStruct', + attrs: array![].span(), + children: array![ + dojo::meta::introspect::Member { + name: 'k1', + attrs: array!['key'].span(), + ty: dojo::meta::introspect::Introspect::::ty() + }, + dojo::meta::introspect::Member { + name: 'v1', + attrs: array![].span(), + ty: dojo::meta::introspect::Introspect::::ty() + }, + dojo::meta::introspect::Member { + name: 'v2', + attrs: array![].span(), + ty: dojo::meta::introspect::Ty::Tuple( + array![ + dojo::meta::introspect::Introspect::::ty(), + dojo::meta::introspect::Introspect::::ty() + ] + .span() + ) + } + ] + .span() + } + ) + } +} diff --git a/crates/dojo/macros/src/tests/derives/introspect.rs b/crates/dojo/macros/src/tests/derives/introspect.rs new file mode 100644 index 0000000000..430495c83e --- /dev/null +++ b/crates/dojo/macros/src/tests/derives/introspect.rs @@ -0,0 +1,201 @@ +use cairo_lang_macro::TokenStream; + +use crate::{derives::handle_derives_macros, tests::utils::assert_output_stream}; + +const SIMPLE_STRUCT: &str = " +#[derive(Introspect)] +struct SimpleStruct { + #[key] + k1: u256, + v1: u32, + v2: (u8, u16), +} +"; + +const EXPANDED_SIMPLE_STRUCT: &str = include_str!("./expanded/simple_struct.cairo"); + +const PACKED_STRUCT: &str = " +#[derive(IntrospectPacked)] +struct SimpleStruct { + #[key] + k1: u256, + v1: u32, + v2: (u8, u16), +} +"; + +const EXPANDED_PACKED_STRUCT: &str = include_str!("./expanded/packed_struct.cairo"); + +const COMPLEX_STRUCT: &str = " +#[derive(Introspect)] +struct ComplexStruct { + #[key] + k1: u256, + #[key] + k2: u32, + v1: Array, + v2: Option, + v3: (Array, u16, Option) +} +"; + +const EXPANDED_COMPLEX_STRUCT: &str = include_str!("./expanded/complex_struct.cairo"); + +const SIMPLE_ENUM: &str = " +#[derive(Introspect)] +enum SimpleEnum { + VARIANT1, + VARIANT2, + VARIANT3 +} +"; + +const EXPANDED_SIMPLE_ENUM: &str = include_str!("./expanded/simple_enum.cairo"); + +const PACKED_ENUM: &str = " +#[derive(Introspect)] +enum PackedEnum { + VARIANT1: (u32, u128), + VARIANT2: (u32, u128), + VARIANT3: (u32, u128), +} +"; + +const EXPANDED_PACKED_ENUM: &str = include_str!("./expanded/packed_enum.cairo"); + +const COMPLEX_ENUM: &str = " +#[derive(Introspect)] +enum ComplexEnum { + VARIANT1: u32, + VARIANT2: Option, + VARIANT3: (u8, u16, u32) +} +"; + +const EXPANDED_COMPLEX_ENUM: &str = include_str!("./expanded/complex_enum.cairo"); + +#[test] +fn test_bad_type() { + let input = TokenStream::new("mod my_module {}".to_string()); + + let res = handle_derives_macros(input); + + assert!(res.diagnostics.is_empty()); + assert!(res.token_stream.is_empty()); +} + +#[test] +fn test_attribute_conflict() { + let input = TokenStream::new( + "#[derive(Introspect, IntrospectPacked)] + struct MyStruct { + v: u32 + }" + .to_string(), + ); + + let res = handle_derives_macros(input); + + assert_eq!( + res.diagnostics[0].message, + format!("Introspect and IntrospectPacked attributes cannot be used at a same time.") + ); +} + +#[test] +fn test_tuple_in_option_error() { + let input = TokenStream::new( + "#[derive(Introspect)] + enum MyEnum { + V1: Option<(u8, u32)> + V2: Option<(u8, u32)> + }" + .to_string(), + ); + + let res = handle_derives_macros(input); + + assert_eq!( + res.diagnostics[0].message, + format!("Option cannot be used with tuples. Prefer using a struct.") + ); +} + +#[test] +fn test_bad_enum_for_introspect_packed() { + let input = TokenStream::new( + "#[derive(IntrospectPacked)] + enum MyEnum { + V1: Option, + V2: u128 + }" + .to_string(), + ); + + let res = handle_derives_macros(input); + + assert_eq!( + res.diagnostics[0].message, + format!("To be packed, all variants must have fixed layout of same size.") + ); +} + +#[test] +fn test_simple_struct() { + let input = TokenStream::new(SIMPLE_STRUCT.to_string()); + + let res = handle_derives_macros(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_SIMPLE_STRUCT); +} + +#[test] +fn test_packed_struct() { + let input = TokenStream::new(PACKED_STRUCT.to_string()); + + let res = handle_derives_macros(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_PACKED_STRUCT); +} + +#[test] +fn test_complex_struct() { + let input = TokenStream::new(COMPLEX_STRUCT.to_string()); + + let res = handle_derives_macros(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_COMPLEX_STRUCT); +} + +#[test] +fn test_simple_enum() { + let input = TokenStream::new(SIMPLE_ENUM.to_string()); + + let res = handle_derives_macros(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_SIMPLE_ENUM); +} + +#[test] +fn test_packed_enum() { + let input = TokenStream::new(PACKED_ENUM.to_string()); + + let res = handle_derives_macros(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_PACKED_ENUM); +} + +#[test] +fn test_complex_enum() { + let input = TokenStream::new(COMPLEX_ENUM.to_string()); + + let res = handle_derives_macros(input); + + assert!(res.diagnostics.is_empty()); + assert_output_stream(&res.token_stream, EXPANDED_COMPLEX_ENUM); +} diff --git a/crates/dojo/macros/src/tests/inlines/selector_from_tag.rs b/crates/dojo/macros/src/tests/inlines/selector_from_tag.rs new file mode 100644 index 0000000000..893d1123b8 --- /dev/null +++ b/crates/dojo/macros/src/tests/inlines/selector_from_tag.rs @@ -0,0 +1,65 @@ +use cairo_lang_macro::TokenStream; +use dojo_types::naming; + +use crate::inlines::selector_from_tag::handle_selector_from_tag_macro; + +#[test] +fn test_with_bad_type() { + let input = TokenStream::new("enum MyEnum { X, Y }".to_string()); + + let res = handle_selector_from_tag_macro(input); + + assert_eq!( + res.diagnostics[0].message, + "Macro `selector_from_tag!` must be called with a string parameter".to_string() + ); +} + +#[test] +fn test_with_bad_argument_type() { + let input = TokenStream::new("selector_from_tag![\"one\"]".to_string()); + + let res = handle_selector_from_tag_macro(input); + + assert_eq!( + res.diagnostics[0].message, + "Macro `selector_from_tag!` does not support this bracket type.".to_string() + ); +} + +#[test] +fn test_with_multiple_arguments() { + let input = TokenStream::new("selector_from_tag!(\"one\", \"two\")".to_string()); + + let res = handle_selector_from_tag_macro(input); + + assert_eq!( + res.diagnostics[0].message, + "Invalid arguments. Expected \"selector_from_tag!(\"tag\")\"".to_string() + ); +} + +#[test] +fn test_with_bad_tag() { + let input = TokenStream::new("selector_from_tag!(\"my_contract\")".to_string()); + + let res = handle_selector_from_tag_macro(input); + + assert_eq!( + res.diagnostics[0].message, + "Invalid tag. Tag must be in the format of `namespace-name`.".to_string() + ); +} + +#[test] +fn test_nominal_case() { + let input = TokenStream::new("selector_from_tag!(\"dojo-my_contract\")".to_string()); + + let res = handle_selector_from_tag_macro(input); + + assert!(res.diagnostics.is_empty()); + assert_eq!( + res.token_stream.to_string(), + format!("{:#64x}", naming::compute_selector_from_tag("dojo-my_contract")) + ); +} diff --git a/crates/dojo/macros/src/tests/mod.rs b/crates/dojo/macros/src/tests/mod.rs new file mode 100644 index 0000000000..2937e9229e --- /dev/null +++ b/crates/dojo/macros/src/tests/mod.rs @@ -0,0 +1,15 @@ +mod attributes { + mod dojo_contract; + mod dojo_event; + mod dojo_model; +} + +mod derives { + mod introspect; +} + +mod inlines { + mod selector_from_tag; +} + +mod utils; diff --git a/crates/dojo/macros/src/tests/utils.rs b/crates/dojo/macros/src/tests/utils.rs new file mode 100644 index 0000000000..dee23f949e --- /dev/null +++ b/crates/dojo/macros/src/tests/utils.rs @@ -0,0 +1,26 @@ +use cairo_lang_macro::TokenStream; +use regex::Regex; + +/// Asserts that the output token stream is as expected. +/// +/// #Arguments +/// `output` - the output token stream +/// `expected` - the expected output +pub(crate) fn assert_output_stream(output: &TokenStream, expected: &str) { + // to avoid differences due to formatting, we remove all the whitespaces + // and newlines. + fn trim_whitespaces_and_newlines(s: &str) -> String { + s.replace(" ", "").replace("\n", "") + } + + // the `ensure_unique` function contains a randomly generated + // hash, so we have to remove it to be able to compare. + let re = Regex::new(r"let _hash =.*;").unwrap(); + let output = output.to_string(); + let output = re.replace(&output, ""); + + let output = trim_whitespaces_and_newlines(&output); + let expected = trim_whitespaces_and_newlines(expected); + + assert_eq!(output, expected); +} diff --git a/crates/dojo/test-utils/src/sequencer.rs b/crates/dojo/test-utils/src/sequencer.rs index 6d876b3f44..43e154e072 100644 --- a/crates/dojo/test-utils/src/sequencer.rs +++ b/crates/dojo/test-utils/src/sequencer.rs @@ -126,5 +126,5 @@ pub fn get_default_test_config(sequencing: SequencingConfig) -> Config { max_event_page_size: Some(100), }; - Config { sequencing, rpc, dev, chain, ..Default::default() } + Config { sequencing, rpc, dev, chain: chain.into(), ..Default::default() } } diff --git a/crates/katana/cli/src/args.rs b/crates/katana/cli/src/args.rs index 0976880839..0474007377 100644 --- a/crates/katana/cli/src/args.rs +++ b/crates/katana/cli/src/args.rs @@ -2,6 +2,7 @@ use std::collections::HashSet; use std::path::PathBuf; +use std::sync::Arc; use alloy_primitives::U256; use anyhow::{Context, Result}; @@ -203,7 +204,7 @@ impl NodeArgs { } } - fn chain_spec(&self) -> Result { + fn chain_spec(&self) -> Result> { let mut chain_spec = chain_spec::DEV_UNALLOCATED.clone(); if let Some(id) = self.starknet.environment.chain_id { @@ -229,7 +230,7 @@ impl NodeArgs { katana_slot_controller::add_controller_account(&mut chain_spec.genesis)?; } - Ok(chain_spec) + Ok(Arc::new(chain_spec)) } fn dev_config(&self) -> DevConfig { diff --git a/crates/katana/core/src/backend/mod.rs b/crates/katana/core/src/backend/mod.rs index 7d8d74a142..47908816bc 100644 --- a/crates/katana/core/src/backend/mod.rs +++ b/crates/katana/core/src/backend/mod.rs @@ -33,7 +33,7 @@ pub(crate) const LOG_TARGET: &str = "katana::core::backend"; #[derive(Debug)] pub struct Backend { - pub chain_spec: ChainSpec, + pub chain_spec: Arc, /// stores all block related data in memory pub blockchain: Blockchain, /// The block context generator. diff --git a/crates/katana/node/src/config/mod.rs b/crates/katana/node/src/config/mod.rs index b79ae25a97..61bf2eeea1 100644 --- a/crates/katana/node/src/config/mod.rs +++ b/crates/katana/node/src/config/mod.rs @@ -5,6 +5,8 @@ pub mod fork; pub mod metrics; pub mod rpc; +use std::sync::Arc; + use db::DbConfig; use dev::DevConfig; use execution::ExecutionConfig; @@ -20,7 +22,7 @@ use rpc::RpcConfig; #[derive(Debug, Clone, Default)] pub struct Config { /// The chain specification. - pub chain: ChainSpec, + pub chain: Arc, /// Database options. pub db: DbConfig, diff --git a/crates/katana/node/src/lib.rs b/crates/katana/node/src/lib.rs index 4675e1acbe..31003668ee 100644 --- a/crates/katana/node/src/lib.rs +++ b/crates/katana/node/src/lib.rs @@ -10,9 +10,8 @@ use std::sync::Arc; use std::time::Duration; use anyhow::Result; -use config::metrics::MetricsConfig; use config::rpc::{ApiKind, RpcConfig}; -use config::{Config, SequencingConfig}; +use config::Config; use dojo_metrics::exporters::prometheus::PrometheusRecorder; use dojo_metrics::{Report, Server as MetricsServer}; use hyper::{Method, Uri}; @@ -28,7 +27,6 @@ use katana_core::constants::{ }; use katana_core::env::BlockContextGenerator; use katana_core::service::block_producer::BlockProducer; -use katana_core::service::messaging::MessagingConfig; use katana_db::mdbx::DbEnv; use katana_executor::implementation::blockifier::BlockifierFactory; use katana_executor::{ExecutionFlags, ExecutorFactory}; @@ -90,10 +88,7 @@ pub struct Node { pub task_manager: TaskManager, pub backend: Arc>, pub block_producer: BlockProducer, - pub rpc_config: RpcConfig, - pub metrics_config: Option, - pub sequencing_config: SequencingConfig, - pub messaging_config: Option, + pub config: Arc, forked_client: Option, } @@ -106,7 +101,7 @@ impl Node { info!(%chain, "Starting node."); // TODO: maybe move this to the build stage - if let Some(ref cfg) = self.metrics_config { + if let Some(ref cfg) = self.config.metrics { let addr = cfg.socket_addr(); let mut reports: Vec> = Vec::new(); @@ -133,7 +128,7 @@ impl Node { backend.clone(), self.task_manager.task_spawner(), block_producer.clone(), - self.messaging_config.clone(), + self.config.messaging.clone(), ); self.task_manager @@ -144,7 +139,7 @@ impl Node { .spawn(sequencing.into_future()); let node_components = (pool, backend, block_producer, validator, self.forked_client.take()); - let rpc = spawn(node_components, self.rpc_config.clone()).await?; + let rpc = spawn(node_components, self.config.rpc.clone()).await?; Ok(LaunchedNode { node: self, rpc }) } @@ -183,8 +178,9 @@ pub async fn build(mut config: Config) -> Result { // --- build backend let (blockchain, db, forked_client) = if let Some(cfg) = &config.forking { + let chain_spec = Arc::get_mut(&mut config.chain).expect("get mut Arc"); let (bc, block_num) = - Blockchain::new_from_forked(cfg.url.clone(), cfg.block, &mut config.chain).await?; + Blockchain::new_from_forked(cfg.url.clone(), cfg.block, chain_spec).await?; // TODO: it'd bee nice if the client can be shared on both the rpc and forked backend side let forked_client = ForkedClient::new_http(cfg.url.clone(), block_num); @@ -201,8 +197,8 @@ pub async fn build(mut config: Config) -> Result { // --- build l1 gas oracle // Check if the user specify a fixed gas price in the dev config. - let gas_oracle = if let Some(fixed_prices) = config.dev.fixed_gas_prices { - L1GasOracle::fixed(fixed_prices.gas_price, fixed_prices.data_gas_price) + let gas_oracle = if let Some(fixed_prices) = &config.dev.fixed_gas_prices { + L1GasOracle::fixed(fixed_prices.gas_price.clone(), fixed_prices.data_gas_price.clone()) } // TODO: for now we just use the default gas prices, but this should be a proper oracle in the // future that can perform actual sampling. @@ -219,7 +215,7 @@ pub async fn build(mut config: Config) -> Result { blockchain, executor_factory, block_context_generator, - chain_spec: config.chain, + chain_spec: config.chain.clone(), }); // --- build block producer @@ -245,10 +241,7 @@ pub async fn build(mut config: Config) -> Result { backend, forked_client, block_producer, - rpc_config: config.rpc, - metrics_config: config.metrics, - messaging_config: config.messaging, - sequencing_config: config.sequencing, + config: Arc::new(config), task_manager: TaskManager::current(), }; diff --git a/crates/torii/types-test/Scarb.lock b/crates/torii/types-test/Scarb.lock index ffc9ecef4d..1080e0820f 100644 --- a/crates/torii/types-test/Scarb.lock +++ b/crates/torii/types-test/Scarb.lock @@ -5,12 +5,12 @@ version = 1 name = "dojo" version = "1.0.3" dependencies = [ - "dojo_plugin", + "dojo_macros", ] [[package]] -name = "dojo_plugin" -version = "2.8.4" +name = "dojo_macros" +version = "0.1.0" [[package]] name = "types_test" diff --git a/examples/game-lib/Scarb.lock b/examples/game-lib/Scarb.lock index 775fb46b9b..10275b4aa2 100644 --- a/examples/game-lib/Scarb.lock +++ b/examples/game-lib/Scarb.lock @@ -17,11 +17,11 @@ dependencies = [ [[package]] name = "dojo" -version = "1.0.0-rc.0" +version = "1.0.1" dependencies = [ - "dojo_plugin", + "dojo_macros", ] [[package]] -name = "dojo_plugin" -version = "2.8.4" +name = "dojo_macros" +version = "0.1.0" diff --git a/examples/simple/Scarb.lock b/examples/simple/Scarb.lock index 3b902cb684..30a1c63616 100644 --- a/examples/simple/Scarb.lock +++ b/examples/simple/Scarb.lock @@ -5,7 +5,7 @@ version = 1 name = "dojo" version = "1.0.2" dependencies = [ - "dojo_plugin", + "dojo_macros", ] [[package]] @@ -16,8 +16,8 @@ dependencies = [ ] [[package]] -name = "dojo_plugin" -version = "2.8.4" +name = "dojo_macros" +version = "0.1.0" [[package]] name = "dojo_simple" diff --git a/examples/simple/src/lib.cairo b/examples/simple/src/lib.cairo index 682d9039c1..096fddd5c8 100644 --- a/examples/simple/src/lib.cairo +++ b/examples/simple/src/lib.cairo @@ -124,8 +124,7 @@ mod tests { fn test_1() { let ndef = NamespaceDef { namespace: "ns", resources: [ - TestResource::Model(m_M::TEST_CLASS_HASH), - TestResource::Contract(c1::TEST_CLASS_HASH), + TestResource::Model("M"), TestResource::Contract("c1"), ].span() }; diff --git a/examples/spawn-and-move/Scarb.lock b/examples/spawn-and-move/Scarb.lock index 1d6835aa49..9026b89f17 100644 --- a/examples/spawn-and-move/Scarb.lock +++ b/examples/spawn-and-move/Scarb.lock @@ -19,7 +19,7 @@ dependencies = [ name = "dojo" version = "1.0.3" dependencies = [ - "dojo_plugin", + "dojo_macros", ] [[package]] @@ -40,5 +40,5 @@ dependencies = [ ] [[package]] -name = "dojo_plugin" -version = "2.8.4" +name = "dojo_macros" +version = "0.1.0" diff --git a/examples/spawn-and-move/src/actions.cairo b/examples/spawn-and-move/src/actions.cairo index 1a8071ada3..c2e03de5b0 100644 --- a/examples/spawn-and-move/src/actions.cairo +++ b/examples/spawn-and-move/src/actions.cairo @@ -221,10 +221,10 @@ mod tests { fn namespace_def() -> NamespaceDef { let ndef = NamespaceDef { namespace: "ns", resources: [ - TestResource::Model(m_Position::TEST_CLASS_HASH), - TestResource::Model(m_Moves::TEST_CLASS_HASH), - TestResource::Event(actions::e_Moved::TEST_CLASS_HASH), - TestResource::Contract(actions::TEST_CLASS_HASH), + TestResource::Model("Position"), + TestResource::Model("Moves"), + TestResource::Event("Moved"), + TestResource::Contract("actions"), ].span() };