diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2cc23698a9..ca62784951 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,6 +44,8 @@ jobs: target: x86_64-pc-windows-msvc - uses: Swatinem/rust-cache@v2 - uses: arduino/setup-protoc@v2 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} - run: cargo build --target x86_64-pc-windows-msvc --bins # cairofmt: diff --git a/Cargo.lock b/Cargo.lock index 5687a0b51e..af542c330f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1339,12 +1339,26 @@ dependencies = [ [[package]] name = "cainome" -version = "0.1.5" -source = "git+https://github.com/cartridge-gg/cainome?rev=950e487#950e4871b735a1b4a7ba7e7561b9a15f5a43dbed" +version = "0.2.3" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.4#a30cc79838ca0d68e24a53a2fdaffa181c849a28" dependencies = [ - "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=950e487)", - "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=950e487)", - "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=950e487)", + "anyhow", + "async-trait", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.4)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.4)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.4)", + "cainome-rs-macro", + "camino", + "clap", + "clap_complete", + "convert_case 0.6.0", + "serde", + "serde_json", + "starknet", + "thiserror", + "tracing", + "tracing-subscriber", + "url", ] [[package]] @@ -1359,7 +1373,7 @@ dependencies = [ [[package]] name = "cainome-cairo-serde" version = "0.1.0" -source = "git+https://github.com/cartridge-gg/cainome?rev=950e487#950e4871b735a1b4a7ba7e7561b9a15f5a43dbed" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.4#a30cc79838ca0d68e24a53a2fdaffa181c849a28" dependencies = [ "starknet", "thiserror", @@ -1380,8 +1394,9 @@ dependencies = [ [[package]] name = "cainome-parser" version = "0.1.0" -source = "git+https://github.com/cartridge-gg/cainome?rev=950e487#950e4871b735a1b4a7ba7e7561b9a15f5a43dbed" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.4#a30cc79838ca0d68e24a53a2fdaffa181c849a28" dependencies = [ + "convert_case 0.6.0", "quote", "serde_json", "starknet", @@ -1408,11 +1423,28 @@ dependencies = [ [[package]] name = "cainome-rs" version = "0.1.0" -source = "git+https://github.com/cartridge-gg/cainome?rev=950e487#950e4871b735a1b4a7ba7e7561b9a15f5a43dbed" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.4#a30cc79838ca0d68e24a53a2fdaffa181c849a28" +dependencies = [ + "anyhow", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.4)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.4)", + "proc-macro2", + "quote", + "serde_json", + "starknet", + "syn 2.0.48", + "thiserror", +] + +[[package]] +name = "cainome-rs-macro" +version = "0.1.0" +source = "git+https://github.com/cartridge-gg/cainome?tag=v0.2.4#a30cc79838ca0d68e24a53a2fdaffa181c849a28" dependencies = [ "anyhow", - "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=950e487)", - "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=950e487)", + "cainome-cairo-serde 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.4)", + "cainome-parser 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.4)", + "cainome-rs 0.1.0 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.4)", "proc-macro2", "quote", "serde_json", @@ -3169,7 +3201,7 @@ name = "dojo-bindgen" version = "0.5.2-alpha.0" dependencies = [ "async-trait", - "cainome 0.1.5 (git+https://github.com/cartridge-gg/cainome?tag=v0.2.2)", + "cainome 0.1.5", "camino", "chrono", "convert_case 0.6.0", @@ -3219,6 +3251,7 @@ dependencies = [ "indoc 1.0.9", "itertools 0.10.5", "lazy_static", + "num-traits 0.2.17", "once_cell", "pretty_assertions", "salsa", @@ -3313,7 +3346,7 @@ dependencies = [ "assert_fs", "assert_matches", "async-trait", - "cainome 0.1.5 (git+https://github.com/cartridge-gg/cainome?rev=950e487)", + "cainome 0.2.3", "cairo-lang-filesystem", "cairo-lang-project", "cairo-lang-starknet", diff --git a/Cargo.toml b/Cargo.toml index f14df97a19..041beb4218 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,6 +136,7 @@ itertools = "0.10.3" jsonrpsee = { version = "0.16.2", default-features = false } lazy_static = "1.4.0" metrics-process = "1.0.9" +num-traits = { version = "0.2", default-features = false } once_cell = "1.0" parking_lot = "0.12.1" pretty_assertions = "1.2.1" diff --git a/README.md b/README.md index 8817c0ad41..b7757f02d2 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,12 @@ We welcome contributions of all kinds from anyone. See our [Contribution Guide]( See our [Environment setup](https://book.dojoengine.org/getting-started) for more information. +## 🛡️Audit + +Dojo core smart contracts have been audited: + +* feb-24: [Nethermind security](https://github.com/NethermindEth/PublicAuditReports/blob/main/NM0159-FINAL_DOJO.pdf) + ## Releasing Propose a new release by manually triggering the `release-dispatch` github action. The version value can be an semver or a level: `[patch, minor, major]`. diff --git a/bin/sozo/src/commands/events.rs b/bin/sozo/src/commands/events.rs index ca9edd556d..48fdb46aed 100644 --- a/bin/sozo/src/commands/events.rs +++ b/bin/sozo/src/commands/events.rs @@ -101,10 +101,6 @@ fn extract_events(manifest: &Manifest) -> HashMap> { inner_helper(&mut events_map, abi); } - if let Some(abi) = manifest.executor.abi.clone() { - inner_helper(&mut events_map, abi); - } - for contract in &manifest.contracts { if let Some(abi) = contract.abi.clone() { inner_helper(&mut events_map, abi); diff --git a/bin/sozo/src/commands/model.rs b/bin/sozo/src/commands/model.rs index 2056ae9157..b4f00d0055 100644 --- a/bin/sozo/src/commands/model.rs +++ b/bin/sozo/src/commands/model.rs @@ -28,6 +28,18 @@ pub enum ModelCommands { starknet: StarknetOptions, }, + #[command(about = "Retrieve the contract address of a model")] + ContractAddress { + #[arg(help = "The name of the model")] + name: String, + + #[command(flatten)] + world: WorldOptions, + + #[command(flatten)] + starknet: StarknetOptions, + }, + #[command(about = "Retrieve the schema for a model")] Schema { #[arg(help = "The name of the model")] diff --git a/bin/sozo/src/ops/migration/migration_test.rs b/bin/sozo/src/ops/migration/migration_test.rs index 14e8a8b658..967ee392df 100644 --- a/bin/sozo/src/ops/migration/migration_test.rs +++ b/bin/sozo/src/ops/migration/migration_test.rs @@ -147,6 +147,5 @@ async fn migration_from_remote() { sequencer.stop().unwrap(); assert_eq!(local_manifest.world.class_hash, remote_manifest.world.class_hash); - assert_eq!(local_manifest.executor.class_hash, remote_manifest.executor.class_hash); assert_eq!(local_manifest.models.len(), remote_manifest.models.len()); } diff --git a/bin/sozo/src/ops/migration/mod.rs b/bin/sozo/src/ops/migration/mod.rs index c2cba7768e..dc92d233ba 100644 --- a/bin/sozo/src/ops/migration/mod.rs +++ b/bin/sozo/src/ops/migration/mod.rs @@ -1,6 +1,7 @@ use std::path::Path; use anyhow::{anyhow, bail, Context, Result}; +use dojo_world::contracts::abi::world::ResourceMetadata; use dojo_world::contracts::cairo_utils; use dojo_world::contracts::world::WorldContract; use dojo_world::manifest::{Manifest, ManifestError}; @@ -286,34 +287,6 @@ where { let ui = ws.config().ui(); - match &strategy.executor { - Some(executor) => { - ui.print_header("# Executor"); - deploy_contract(executor, "executor", vec![], migrator, &ui, &txn_config).await?; - - // There is no world migration, so it exists already. - if strategy.world.is_none() { - let addr = strategy.world_address()?; - let InvokeTransactionResult { transaction_hash } = - WorldContract::new(addr, &migrator) - .set_executor(&executor.contract_address.into()) - .send() - .await - .map_err(|e| { - ui.verbose(format!("{e:?}")); - e - })?; - - TransactionWaiter::new(transaction_hash, migrator.provider()).await?; - - ui.print_hidden_sub(format!("Updated at: {transaction_hash:#x}")); - } - - ui.print_sub(format!("Contract address: {:#x}", executor.contract_address)); - } - None => {} - }; - match &strategy.base { Some(base) => { ui.print_header("# Base Contract"); @@ -341,10 +314,7 @@ where Some(world) => { ui.print_header("# World"); - let calldata = vec![ - strategy.executor.as_ref().unwrap().contract_address, - strategy.base.as_ref().unwrap().diff.local, - ]; + let calldata = vec![strategy.base.as_ref().unwrap().diff.local]; deploy_contract(world, "world", calldata.clone(), migrator, &ui, &txn_config) .await .map_err(|e| { @@ -358,11 +328,21 @@ where if let Some(meta) = metadata.as_ref().and_then(|inner| inner.world()) { match meta.upload().await { Ok(hash) => { - let encoded_uri = cairo_utils::encode_uri(&format!("ipfs://{hash}"))?; + let mut encoded_uri = cairo_utils::encode_uri(&format!("ipfs://{hash}"))?; + + // Metadata is expecting an array of capacity 3. + if encoded_uri.len() < 3 { + encoded_uri.extend(vec![FieldElement::ZERO; 3 - encoded_uri.len()]); + } + + let world_metadata = ResourceMetadata { + resource_id: FieldElement::ZERO, + metadata_uri: encoded_uri, + }; let InvokeTransactionResult { transaction_hash } = WorldContract::new(world.contract_address, migrator) - .set_metadata_uri(&FieldElement::ZERO, &encoded_uri) + .set_metadata(&world_metadata) .send() .await .map_err(|e| { @@ -382,6 +362,9 @@ where None => {} }; + // Once Torii supports indexing arrays, we should declare and register the + // ResourceMetadata model. + register_models(strategy, migrator, &ui, txn_config.clone()).await?; deploy_contracts(strategy, migrator, &ui, txn_config).await?; diff --git a/bin/sozo/src/ops/model.rs b/bin/sozo/src/ops/model.rs index a6cce2444d..a4a38441e0 100644 --- a/bin/sozo/src/ops/model.rs +++ b/bin/sozo/src/ops/model.rs @@ -20,6 +20,18 @@ pub async fn execute(command: ModelCommands, env_metadata: Option) println!("{:#x}", model.class_hash()); } + ModelCommands::ContractAddress { name, world, starknet } => { + let world_address = world.address(env_metadata.as_ref())?; + let provider = starknet.provider(env_metadata.as_ref())?; + + let world = WorldContractReader::new(world_address, &provider) + .with_block(BlockId::Tag(BlockTag::Pending)); + + let model = world.model_reader(&name).await?; + + println!("{:#x}", model.contract_address()); + } + ModelCommands::Schema { name, world, starknet, to_json } => { let world_address = world.address(env_metadata.as_ref())?; let provider = starknet.provider(env_metadata.as_ref())?; diff --git a/bin/sozo/tests/test_data/manifest.json b/bin/sozo/tests/test_data/manifest.json index d6ec585683..62b78c9e70 100644 --- a/bin/sozo/tests/test_data/manifest.json +++ b/bin/sozo/tests/test_data/manifest.json @@ -843,6 +843,15 @@ "writes": [], "computed": [] }, + "resource_metadata": { + "name": "", + "address": null, + "class_hash": "0x0", + "abi": null, + "reads": [], + "writes": [], + "computed": [] + }, "base": { "name": "dojo::base::base", "class_hash": "0x6c458453d35753703ad25632deec20a29faf8531942ec109e6eb0650316a2bc", @@ -1569,4 +1578,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/crates/dojo-core/src/base_test.cairo b/crates/dojo-core/src/base_test.cairo index d2f1e9dcde..2fe4a4ce1c 100644 --- a/crates/dojo-core/src/base_test.cairo +++ b/crates/dojo-core/src/base_test.cairo @@ -1,3 +1,4 @@ +use debug::PrintTrait; use option::OptionTrait; use starknet::ClassHash; use traits::TryInto; @@ -10,6 +11,8 @@ use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait}; #[starknet::contract] mod contract_upgrade { + use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, IWorldProvider}; + #[storage] struct Storage {} @@ -27,6 +30,19 @@ mod contract_upgrade { 'daddy' } } + + #[abi(embed_v0)] + impl WorldProviderImpl of IWorldProvider { + fn world(self: @ContractState) -> IWorldDispatcher { + IWorldDispatcher { contract_address: starknet::contract_address_const::<'world'>() } + } + } +} + +#[starknet::contract] +mod contract_invalid_upgrade { + #[storage] + struct Storage {} } use contract_upgrade::{IQuantumLeapDispatcher, IQuantumLeapDispatcherTrait}; @@ -50,6 +66,18 @@ fn test_upgrade_from_world() { assert(quantum_dispatcher.plz_more_tps() == 'daddy', 'quantum leap failed'); } +#[test] +#[available_gas(6000000)] +#[should_panic(expected: ('class_hash not world provider', 'ENTRYPOINT_FAILED', 'ENTRYPOINT_FAILED'))] +fn test_upgrade_from_world_not_world_provider() { + let world = deploy_world(); + + let base_address = world.deploy_contract('salt', base::TEST_CLASS_HASH.try_into().unwrap()); + let new_class_hash: ClassHash = contract_invalid_upgrade::TEST_CLASS_HASH.try_into().unwrap(); + + world.upgrade_contract(base_address, new_class_hash); +} + #[test] #[available_gas(6000000)] #[should_panic(expected: ('must be called by world', 'ENTRYPOINT_FAILED'))] @@ -62,3 +90,59 @@ fn test_upgrade_direct() { let upgradeable_dispatcher = IUpgradeableDispatcher { contract_address: base_address }; upgradeable_dispatcher.upgrade(new_class_hash); } + +#[starknet::interface] +trait INameOnly { + fn name(self: @T) -> felt252; +} + +#[starknet::contract] +mod invalid_model { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl InvalidModelName of super::INameOnly { + fn name(self: @ContractState) -> felt252 { + // Pre-computed address of a contract deployed through the world. + 0x34534b116332dd9459bfde65280822d84c130e3f1faeb63af8455f83e733f4f + } + } +} + +#[starknet::contract] +mod invalid_model_world { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl InvalidModelName of super::INameOnly { + fn name(self: @ContractState) -> felt252 { + // World address is 0, and not registered as deployed through the world + // as it's itself. + 0 + } + } +} + +#[test] +#[available_gas(6000000)] +#[should_panic(expected: ('invalid model name', 'ENTRYPOINT_FAILED',))] +fn test_deploy_from_world_invalid_model() { + let world = deploy_world(); + + let base_address = world.deploy_contract(0, base::TEST_CLASS_HASH.try_into().unwrap()); + // The print is required for invalid_model name to be a valid address as the + // register_model will use the gas consumed as salt. + base_address.print(); + + world.register_model(invalid_model::TEST_CLASS_HASH.try_into().unwrap()); +} + +#[test] +#[available_gas(6000000)] +#[should_panic(expected: ('invalid model name', 'ENTRYPOINT_FAILED',))] +fn test_deploy_from_world_invalid_model_world() { + let world = deploy_world(); + world.register_model(invalid_model_world::TEST_CLASS_HASH.try_into().unwrap()); +} diff --git a/crates/dojo-core/src/benchmarks.cairo b/crates/dojo-core/src/benchmarks.cairo index b2a206b54c..03c5ca2bab 100644 --- a/crates/dojo-core/src/benchmarks.cairo +++ b/crates/dojo-core/src/benchmarks.cairo @@ -2,13 +2,12 @@ use core::result::ResultTrait; use array::ArrayTrait; use array::SpanTrait; use debug::PrintTrait; -use option::OptionTrait; use poseidon::poseidon_hash_span; use starknet::SyscallResultTrait; use starknet::{contract_address_const, ContractAddress, ClassHash, get_caller_address}; use dojo::database; -use dojo::database::{storage, index}; +use dojo::database::storage; use dojo::model::Model; use dojo::world_test::Foo; use dojo::test_utils::end; @@ -49,12 +48,12 @@ fn bench_storage_many() { let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - storage::set_many(0, keys, 0, values, layout); + storage::set_many(0, keys, values, layout).unwrap(); end(gas, 'storage set mny'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - let res = storage::get_many(0, keys, 0, 2, layout); + let res = storage::get_many(0, keys, layout).unwrap(); end(gas, 'storage get mny'); assert(res.len() == 2, 'wrong number of values'); @@ -110,171 +109,47 @@ fn bench_native_storage_offset() { #[test] #[available_gas(1000000000)] -fn bench_index() { - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - let no_query = index::query(0, 69, Option::None(())); - end(gas, 'idx empty'); - assert(no_query.len() == 0, 'entity indexed'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - index::create(0, 69, 420); - end(gas, 'idx create 1st'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - let query = index::query(0, 69, Option::None(())); - end(gas, 'idx query one'); - assert(query.len() == 1, 'entity not indexed'); - assert(*query.at(0) == 420, 'entity value incorrect'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - index::create(0, 69, 1337); - end(gas, 'idx query 2nd'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - let two_query = index::query(0, 69, Option::None(())); - end(gas, 'idx query two'); - assert(two_query.len() == 2, 'index should have two query'); - assert(*two_query.at(1) == 1337, 'entity value incorrect'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - index::exists(0, 69, 420); - end(gas, 'idx exists chk'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - index::delete(0, 69, 420); - end(gas, 'idx dlt !last'); - - assert(!index::exists(0, 69, 420), 'entity should not exist'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - index::delete(0, 69, 1337); - end(gas, 'idx dlt last'); +fn bench_database_array() { + let mut keys = ArrayTrait::new(); + keys.append(0x966); - assert(!index::exists(0, 69, 1337), 'entity should not exist'); -} + let array_test_len: usize = 300; -#[test] -#[available_gas(1000000000)] -fn bench_big_index() { - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); + let mut layout = ArrayTrait::new(); + let mut values: Array = ArrayTrait::new(); let mut i = 0; loop { - if i == 1000 { + if i == array_test_len { break; } - index::create(0, 69, i); - i += 1; - }; - end(gas, 'idx create 1000'); - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - let query = index::query(0, 69, Option::None(())); - end(gas, 'idx query 1000'); - assert(query.len() == 1000, 'entity not indexed'); - assert(*query.at(420) == 420, 'entity value incorrect'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - index::exists(0, 69, 999); - end(gas, 'idx exists 1000'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - index::delete(0, 69, 999); - end(gas, 'idx dlt 1000'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - index::delete(0, 69, 420); - end(gas, 'idx dlt !1000 >'); + values.append(i.into()); + layout.append(251_u8); - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - index::delete(0, 69, 420); - end(gas, 'idx dlt !1000 0'); -} - -#[test] -#[available_gas(1000000000)] -fn bench_database_array() { - let value = array![1, 2, 3, 4, 5, 6, 7, 8, 9, 10].span(); - let layout = array![251, 251, 251, 251, 251, 251, 251, 251, 251, 251].span(); - let half_layout = array![251, 251, 251, 251, 251].span(); - let len = value.len(); + i += 1; + }; let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - database::set('table', 'key', 0, value, layout); + database::set('table', 'key', values.span(), layout.span()); end(gas, 'db set arr'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - let res = database::get('table', 'key', 0, len, layout); + let res = database::get('table', 'key', layout.span()); end(gas, 'db get arr'); - assert(res.len() == len, 'wrong number of values'); - assert(*res.at(0) == *value.at(0), 'value not set'); - assert(*res.at(1) == *value.at(1), 'value not set'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - let second_res = database::get('table', 'key', 3, 8, half_layout); - end(gas, 'db get half arr'); - - assert(second_res.len() == 5, 'wrong number of values'); - assert(*second_res.at(0) == *value.at(3), 'value not set'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - database::del('table', 'key'); - end(gas, 'db del arr'); -} - -#[test] -#[available_gas(1000000000)] -fn bench_indexed_database_array() { - let even = array![2, 4].span(); - let odd = array![1, 3].span(); - let layout = array![251, 251].span(); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - database::set_with_index('table', 'even', 0, even, layout); - end(gas, 'dbi set arr 1st'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - let (_keys, _values) = database::scan('table', Option::None(()), 2, layout); - end(gas, 'dbi scan arr 1'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - database::set_with_index('table', 'odd', 0, odd, layout); - end(gas, 'dbi set arr 2nd'); - - let gas = testing::get_available_gas(); - gas::withdraw_gas().unwrap(); - let (keys, values) = database::scan('table', Option::None(()), 2, layout); - end(gas, 'dbi scan arr 2'); + let mut i = 0; + loop { + if i == array_test_len { + break; + } - assert(keys.len() == 2, 'Wrong number of keys!'); - assert(values.len() == 2, 'Wrong number of values!'); - assert(*keys.at(0) == 'even', 'Wrong key at index 0!'); - assert(*(*values.at(0)).at(0) == 2, 'Wrong value at index 0!'); - assert(*(*values.at(0)).at(1) == 4, 'Wrong value at index 1!'); + assert(res.at(i) == values.at(i), 'Value not equal!'); + i += 1; + }; } - #[test] #[available_gas(1000000000)] fn bench_simple_struct() { @@ -369,12 +244,12 @@ fn test_struct_with_many_fields() { let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - database::set('positions', '42', 0, pos.values(), pos.layout()); + database::set('positions', '42', pos.values(), pos.layout()); end(gas, 'pos db set'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - database::get('positions', '42', 0, pos.packed_size(), pos.layout()); + database::get('positions', '42', pos.layout()); end(gas, 'pos db get'); } @@ -439,12 +314,12 @@ fn bench_nested_struct() { let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - database::set('cases', '42', 0, case.values(), case.layout()); + database::set('cases', '42', values, case.layout()); end(gas, 'case db set'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - database::get('cases', '42', 0, case.packed_size(), case.layout()); + database::get('cases', '42', case.layout()); end(gas, 'case db get'); } @@ -559,11 +434,11 @@ fn bench_complex_struct() { let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - database::set('chars', '42', 0, char.values(), char.layout()); + database::set('chars', '42', char.values(), char.layout()); end(gas, 'chars db set'); let gas = testing::get_available_gas(); gas::withdraw_gas().unwrap(); - database::get('chars', '42', 0, char.packed_size(), char.layout()); + database::get('chars', '42', char.layout()); end(gas, 'chars db get'); } diff --git a/crates/dojo-core/src/components/upgradeable.cairo b/crates/dojo-core/src/components/upgradeable.cairo index c5a030f12f..1dcb59aaf0 100644 --- a/crates/dojo-core/src/components/upgradeable.cairo +++ b/crates/dojo-core/src/components/upgradeable.cairo @@ -7,6 +7,7 @@ trait IUpgradeable { #[starknet::component] mod upgradeable { + use core::starknet::SyscallResultTrait; use starknet::ClassHash; use starknet::ContractAddress; use starknet::get_caller_address; @@ -29,10 +30,13 @@ mod upgradeable { mod Errors { const INVALID_CLASS: felt252 = 'class_hash cannot be zero'; + const INVALID_CLASS_CONTENT: felt252 = 'class_hash not world provider'; const INVALID_CALLER: felt252 = 'must be called by world'; const INVALID_WORLD_ADDRESS: felt252 = 'invalid world address'; } + use debug::PrintTrait; + #[embeddable_as(UpgradableImpl)] impl Upgradable< TContractState, +HasComponent, +IWorldProvider @@ -48,9 +52,17 @@ mod upgradeable { ); assert(new_class_hash.is_non_zero(), Errors::INVALID_CLASS); - replace_class_syscall(new_class_hash).unwrap(); - - self.emit(Upgraded { class_hash: new_class_hash }); + match starknet::library_call_syscall( + new_class_hash, + selector!("world"), + array![].span(), + ) { + Result::Ok(_) => { + replace_class_syscall(new_class_hash).unwrap(); + self.emit(Upgraded { class_hash: new_class_hash }); + }, + Result::Err(_) => panic_with_felt252(Errors::INVALID_CLASS_CONTENT), + } } } } diff --git a/crates/dojo-core/src/database.cairo b/crates/dojo-core/src/database.cairo index 86b6ec6136..87c33752d5 100644 --- a/crates/dojo-core/src/database.cairo +++ b/crates/dojo-core/src/database.cairo @@ -3,101 +3,29 @@ use traits::{Into, TryInto}; use serde::Serde; use hash::LegacyHash; use poseidon::poseidon_hash_span; +use starknet::SyscallResultTrait; + +const DOJO_STORAGE: felt252 = 'dojo_storage'; -mod index; -#[cfg(test)] -mod index_test; mod introspect; #[cfg(test)] mod introspect_test; mod storage; #[cfg(test)] mod storage_test; -mod utils; -#[cfg(test)] -mod utils_test; - -use index::WhereCondition; -fn get(table: felt252, key: felt252, offset: u8, length: usize, layout: Span) -> Span { +fn get(table: felt252, key: felt252, layout: Span) -> Span { let mut keys = ArrayTrait::new(); - keys.append('dojo_storage'); + keys.append(DOJO_STORAGE); keys.append(table); keys.append(key); - storage::get_many(0, keys.span(), offset, length, layout) + storage::get_many(0, keys.span(), layout).unwrap_syscall() } -fn set(table: felt252, key: felt252, offset: u8, value: Span, layout: Span) { +fn set(table: felt252, key: felt252, value: Span, layout: Span) { let mut keys = ArrayTrait::new(); - keys.append('dojo_storage'); + keys.append(DOJO_STORAGE); keys.append(table); keys.append(key); - storage::set_many(0, keys.span(), offset, value, layout); -} - -fn set_with_index( - table: felt252, key: felt252, offset: u8, value: Span, layout: Span -) { - set(table, key, offset, value, layout); - index::create(0, table, key); -} - -fn del(table: felt252, key: felt252) { - index::delete(0, table, key); -} - -// Query all entities that meet a criteria. If no index is defined, -// Returns a tuple of spans, first contains the entity IDs, -// second the deserialized entities themselves. -fn scan( - model: felt252, where: Option, values_length: usize, values_layout: Span -) -> (Span, Span>) { - let all_ids = scan_ids(model, where); - (all_ids, get_by_ids(model, all_ids, values_length, values_layout)) -} - -/// Analogous to `scan`, but returns only the IDs of the entities. -fn scan_ids(model: felt252, where: Option) -> Span { - match where { - Option::Some(clause) => { - let mut serialized = ArrayTrait::new(); - model.serialize(ref serialized); - clause.key.serialize(ref serialized); - let index = poseidon_hash_span(serialized.span()); - - index::get_by_key(0, index, clause.value).span() - }, - // If no `where` clause is defined, we return all values. - Option::None(_) => { - index::query(0, model, Option::None) - } - } -} - -/// Returns entries on the given ids. -/// # Arguments -/// * `class_hash` - The class hash of the contract. -/// * `table` - The table to get the entries from. -/// * `all_ids` - The ids of the entries to get. -/// * `length` - The length of the entries. -fn get_by_ids( - table: felt252, all_ids: Span, length: u32, layout: Span -) -> Span> { - let mut entities: Array> = ArrayTrait::new(); - let mut ids = all_ids; - loop { - match ids.pop_front() { - Option::Some(id) => { - let mut keys = ArrayTrait::new(); - keys.append('dojo_storage'); - keys.append(table); - keys.append(*id); - let value: Span = storage::get_many(0, keys.span(), 0_u8, length, layout); - entities.append(value); - }, - Option::None(_) => { - break entities.span(); - } - }; - } + storage::set_many(0, keys.span(), value, layout).unwrap_syscall(); } diff --git a/crates/dojo-core/src/database/index.cairo b/crates/dojo-core/src/database/index.cairo deleted file mode 100644 index 4a1a514fc4..0000000000 --- a/crates/dojo-core/src/database/index.cairo +++ /dev/null @@ -1,154 +0,0 @@ -use array::{ArrayTrait, SpanTrait}; -use traits::Into; -use option::OptionTrait; -use poseidon::poseidon_hash_span; -use serde::Serde; - -use dojo::database::storage; - -#[derive(Copy, Drop)] -struct WhereCondition { - key: felt252, - value: felt252, -} - -fn create(address_domain: u32, index: felt252, id: felt252) { - if exists(address_domain, index, id) { - return (); - } - - let index_len_key = build_index_len_key(index); - let index_len = storage::get(address_domain, index_len_key); - storage::set(address_domain, build_index_item_key(index, id), index_len + 1); - storage::set(address_domain, index_len_key, index_len + 1); - storage::set(address_domain, build_index_key(index, index_len), id); -} - -/// Deletes an entry from the main index, as well as from each of the keys. -/// # Arguments -/// * address_domain - The address domain to write to. -/// * index - The index to write to. -/// * id - The id of the entry. -/// # Returns -fn delete(address_domain: u32, index: felt252, id: felt252) { - if !exists(address_domain, index, id) { - return (); - } - - let index_len_key = build_index_len_key(index); - let replace_item_idx = storage::get(address_domain, index_len_key) - 1; - - let index_item_key = build_index_item_key(index, id); - let delete_item_idx = storage::get(address_domain, index_item_key) - 1; - - storage::set(address_domain, index_item_key, 0); - storage::set(address_domain, index_len_key, replace_item_idx); - - // Replace the deleted element with the last element. - // NOTE: We leave the last element set as to not produce an unncessary state diff. - let replace_item_value = storage::get(address_domain, build_index_key(index, replace_item_idx)); - storage::set(address_domain, build_index_key(index, delete_item_idx), replace_item_value); -} - -fn exists(address_domain: u32, index: felt252, id: felt252) -> bool { - storage::get(address_domain, build_index_item_key(index, id)) != 0 -} - -fn query(address_domain: u32, table: felt252, where: Option) -> Span { - let mut res = ArrayTrait::new(); - - match where { - Option::Some(clause) => { - let mut serialized = ArrayTrait::new(); - table.serialize(ref serialized); - clause.key.serialize(ref serialized); - let index = poseidon_hash_span(serialized.span()); - - let index_len_key = build_index_len_key(index); - let index_len = storage::get(address_domain, index_len_key); - let mut idx = 0; - - loop { - if idx == index_len { - break (); - } - let id = storage::get(address_domain, build_index_key(index, idx)); - res.append(id); - } - }, - - // If no `where` clause is defined, we return all values. - Option::None(_) => { - let index_len_key = build_index_len_key(table); - let index_len = storage::get(address_domain, index_len_key); - let mut idx = 0; - - loop { - if idx == index_len { - break (); - } - - res.append(storage::get(address_domain, build_index_key(table, idx))); - idx += 1; - }; - } - } - - res.span() -} - -/// Returns all the entries that hold a given key -/// # Arguments -/// * address_domain - The address domain to write to. -/// * index - The index to read from. -/// * key - The key return values from. -fn get_by_key(address_domain: u32, index: felt252, key: felt252) -> Array { - let mut res = ArrayTrait::new(); - let specific_len_key = build_index_specific_key_len(index, key); - let index_len = storage::get(address_domain, specific_len_key); - - let mut idx = 0; - - loop { - if idx == index_len { - break (); - } - - let specific_key = build_index_specific_key(index, key, idx); - let id = storage::get(address_domain, specific_key); - res.append(id); - - idx += 1; - }; - - res -} - -fn build_index_len_key(index: felt252) -> Span { - array!['dojo_index_lens', index].span() -} - -fn build_index_key(index: felt252, idx: felt252) -> Span { - array!['dojo_indexes', index, idx].span() -} - -fn build_index_item_key(index: felt252, id: felt252) -> Span { - array!['dojo_index_ids', index, id].span() -} - -/// Key for a length of index for a given key. -/// # Arguments -/// * index - The index to write to. -/// * key - The key to write. -fn build_index_specific_key_len(index: felt252, key: felt252) -> Span { - array!['dojo_index_key_len', index, key].span() -} - -/// Key for an index of a given key. -/// # Arguments -/// * index - The index to write to. -/// * key - The key to write. -/// * idx - The position in the index. -fn build_index_specific_key(index: felt252, key: felt252, idx: felt252) -> Span { - array!['dojo_index_key', index, key, idx].span() -} \ No newline at end of file diff --git a/crates/dojo-core/src/database/index_test.cairo b/crates/dojo-core/src/database/index_test.cairo deleted file mode 100644 index 2d3aa22a6b..0000000000 --- a/crates/dojo-core/src/database/index_test.cairo +++ /dev/null @@ -1,68 +0,0 @@ -use array::ArrayTrait; -use traits::Into; -use debug::PrintTrait; -use option::OptionTrait; - - -use dojo::database::index; - -#[test] -#[available_gas(2000000)] -fn test_index_entity() { - let no_query = index::query(0, 69, Option::None(())); - assert(no_query.len() == 0, 'entity indexed'); - - index::create(0, 69, 420); - let query = index::query(0, 69, Option::None(())); - assert(query.len() == 1, 'entity not indexed'); - assert(*query.at(0) == 420, 'entity value incorrect'); - - index::create(0, 69, 420); - let noop_query = index::query(0, 69, Option::None(())); - assert(noop_query.len() == 1, 'index should be noop'); - - index::create(0, 69, 1337); - let two_query = index::query(0, 69, Option::None(())); - assert(two_query.len() == 2, 'index should have two query'); - assert(*two_query.at(1) == 1337, 'entity value incorrect'); -} - -#[test] -#[available_gas(2000000)] -fn test_entity_delete_basic() { - index::create(0, 69, 420); - let query = index::query(0, 69, Option::None(())); - assert(query.len() == 1, 'entity not indexed'); - assert(*query.at(0) == 420, 'entity value incorrect'); - - assert(index::exists(0, 69, 420), 'entity should exist'); - - index::delete(0, 69, 420); - - assert(!index::exists(0, 69, 420), 'entity should not exist'); - let no_query = index::query(0, 69, Option::None(())); - assert(no_query.len() == 0, 'index should have no query'); -} - -#[test] -#[available_gas(20000000)] -fn test_entity_query_delete_shuffle() { - let table = 1; - index::create(0, table, 10); - index::create(0, table, 20); - index::create(0, table, 30); - assert(index::query(0, table, Option::None(())).len() == 3, 'wrong size'); - - index::delete(0, table, 10); - let entities = index::query(0, table, Option::None(())); - assert(entities.len() == 2, 'wrong size'); - assert(*entities.at(0) == 30, 'idx 0 not 30'); - assert(*entities.at(1) == 20, 'idx 1 not 20'); -} - -#[test] -#[available_gas(20000000)] -fn test_entity_query_delete_non_existing() { - assert(index::query(0, 69, Option::None(())).len() == 0, 'table len != 0'); - index::delete(0, 69, 999); // deleting non-existing should not panic -} diff --git a/crates/dojo-core/src/database/introspect.cairo b/crates/dojo-core/src/database/introspect.cairo index 3d0af0d90b..dfb592c852 100644 --- a/crates/dojo-core/src/database/introspect.cairo +++ b/crates/dojo-core/src/database/introspect.cairo @@ -4,6 +4,8 @@ enum Ty { Struct: Struct, Enum: Enum, Tuple: Span>, + // Store the capacity of the array. + Array: u32, } #[derive(Copy, Drop, Serde)] diff --git a/crates/dojo-core/src/database/storage.cairo b/crates/dojo-core/src/database/storage.cairo index 9052b0b02f..aa3457d73a 100644 --- a/crates/dojo-core/src/database/storage.cairo +++ b/crates/dojo-core/src/database/storage.cairo @@ -1,10 +1,10 @@ use array::{ArrayTrait, SpanTrait}; use option::OptionTrait; -use starknet::SyscallResultTrait; +use starknet::{SyscallResultTrait, StorageAddress, StorageBaseAddress, SyscallResult}; use traits::Into; use poseidon::poseidon_hash_span; use serde::Serde; -use dojo::packing::{pack, unpack}; +use dojo::packing::{pack, unpack, calculate_packed_size}; fn get(address_domain: u32, keys: Span) -> felt252 { let base = starknet::storage_base_address_from_felt252(poseidon_hash_span(keys)); @@ -12,32 +12,52 @@ fn get(address_domain: u32, keys: Span) -> felt252 { .unwrap_syscall() } -fn get_many(address_domain: u32, keys: Span, offset: u8, length: usize, mut layout: Span) -> Span { +fn get_many(address_domain: u32, keys: Span, mut layout: Span) -> SyscallResult> { let base = starknet::storage_base_address_from_felt252(poseidon_hash_span(keys)); + let base_address = starknet::storage_address_from_base(base); + let mut packed = ArrayTrait::new(); - let mut offset = offset; - loop { - if length == offset.into() { - break (); - } + let mut layout_calculate = layout; + let len: usize = calculate_packed_size(ref layout_calculate); + + let mut chunk = 0; + let mut chunk_base = base; + let mut index_in_chunk = 0_u8; + + let mut packed_span = loop { + let value = + match starknet::syscalls::storage_read_syscall( + address_domain, starknet::storage_address_from_base_and_offset(chunk_base, index_in_chunk) + ) { + Result::Ok(value) => value, + Result::Err(err) => { break SyscallResult::>::Err(err); }, + }; - packed - .append( - starknet::storage_read_syscall( - address_domain, starknet::storage_address_from_base_and_offset(base, offset) - ) - .unwrap_syscall() - ); + packed.append(value); - offset += 1; - }; + // Verify first the length to avoid computing the new chunk segment + // if not required. + if packed.len() == len { + break SyscallResult::>::Ok(packed.span()); + } + + index_in_chunk = match core::integer::u8_overflowing_add(index_in_chunk, 1) { + Result::Ok(x) => x, + Result::Err(_) => { + // After reading 256 `felt`s, `index_in_chunk` will overflow and we move to the + // next chunk. + chunk += 1; + chunk_base = chunk_segment_pointer(base_address, chunk); + 0 + }, + }; + }?; - let mut packed = packed.span(); let mut unpacked = ArrayTrait::new(); - unpack(ref unpacked, ref packed, ref layout); + unpack(ref unpacked, ref packed_span, ref layout); - unpacked.span() + Result::Ok(unpacked.span()) } @@ -48,24 +68,48 @@ fn set(address_domain: u32, keys: Span, value: felt252) { ).unwrap_syscall(); } -fn set_many(address_domain: u32, keys: Span, offset: u8, mut unpacked: Span, mut layout: Span) { +fn set_many(address_domain: u32, keys: Span, mut unpacked: Span, mut layout: Span) -> SyscallResult<()> { let base = starknet::storage_base_address_from_felt252(poseidon_hash_span(keys)); + let base_address = starknet::storage_address_from_base(base); let mut packed = ArrayTrait::new(); pack(ref packed, ref unpacked, ref layout); - let mut offset = offset; + let mut chunk = 0; + let mut chunk_base = base; + let mut index_in_chunk = 0_u8; + loop { - match packed.pop_front() { - Option::Some(v) => { - starknet::storage_write_syscall( - address_domain, starknet::storage_address_from_base_and_offset(base, offset), v - ).unwrap_syscall(); - offset += 1 - }, - Option::None(_) => { - break (); + let curr_value = match packed.pop_front() { + Option::Some(x) => x, + Option::None => { break Result::Ok(()); }, + }; + + match starknet::syscalls::storage_write_syscall( + address_domain, + starknet::storage_address_from_base_and_offset(chunk_base, index_in_chunk), + curr_value.into() + ) { + Result::Ok(_) => {}, + Result::Err(err) => { break Result::Err(err); }, + }; + + index_in_chunk = match core::integer::u8_overflowing_add(index_in_chunk, 1) { + Result::Ok(x) => x, + Result::Err(_) => { + // After writing 256 `felt`s, `index_in_chunk` will overflow and we move to the + // next chunk which will be stored in an other storage segment. + chunk += 1; + chunk_base = chunk_segment_pointer(base_address, chunk); + 0 + }, }; - }; + } +} + +fn chunk_segment_pointer(address: StorageAddress, chunk: felt252) -> StorageBaseAddress { + let p = poseidon_hash_span(array![address.into(), chunk, 'DojoStorageChunk'].span()); + //let (r, _, _) = core::poseidon::hades_permutation(address.into(), chunk, 'DojoStorageChunk'_felt252); + starknet::storage_base_address_from_felt252(p) } diff --git a/crates/dojo-core/src/database/storage_test.cairo b/crates/dojo-core/src/database/storage_test.cairo index 61aab0b8c6..0010290e28 100644 --- a/crates/dojo-core/src/database/storage_test.cairo +++ b/crates/dojo-core/src/database/storage_test.cairo @@ -15,40 +15,33 @@ fn test_storage() { values.append(0x1); values.append(0x2); + let layout = array![251, 251].span(); + storage::set(0, keys.span(), *values.at(0)); assert(storage::get(0, keys.span()) == *values.at(0), 'value not set'); - storage::set_many(0, keys.span(), 0, values.span(), array![251, 251].span()); - let res = storage::get_many(0, keys.span(), 0, 2, array![251, 251].span()); + storage::set_many(0, keys.span(), values.span(), layout).unwrap(); + let res = storage::get_many(0, keys.span(), layout).unwrap(); assert(*res.at(0) == *values.at(0), 'value not set'); + assert(*res.at(1) == *values.at(1), 'value not set'); } #[test] -#[available_gas(2000000)] +#[available_gas(20000000)] fn test_storage_empty() { let mut keys = ArrayTrait::new(); assert(storage::get(0, keys.span()) == 0x0, 'Value should be 0'); - let many = storage::get_many(0, keys.span(), 0, 3, array![251, 251, 251].span()); - assert(*many.at(0) == 0x0, 'Value should be 0'); - assert(*many.at(1) == 0x0, 'Value should be 0'); - assert(*many.at(2) == 0x0, 'Value should be 0'); -} + let many = storage::get_many(0, keys.span(), array![251, 251, 251].span()).unwrap(); + assert(many.len() == 0x3, 'Array should be len 3'); + assert((*many[0]) == 0x0, 'Array[0] should be 0'); + assert((*many[1]) == 0x0, 'Array[1] should be 0'); + assert((*many[2]) == 0x0, 'Array[2] should be 0'); -#[test] -#[available_gas(100000000)] -fn test_storage_get_many_length() { - let mut keys = ArrayTrait::new(); - let mut layout = array![]; - let mut i = 0_usize; - loop { - if i >= 30 { - break; - }; - - layout.append(251); - assert(storage::get_many(0, keys.span(), 0, i, layout.span()).len() == i, 'Values should be equal!'); - i += 1; - }; + let many = storage::get_many(0, keys.span(), array![8, 8, 32].span()).unwrap(); + assert(many.len() == 0x3, 'Array should be len 3'); + assert((*many[0]) == 0x0, 'Array[0] should be 0'); + assert((*many[1]) == 0x0, 'Array[1] should be 0'); + assert((*many[2]) == 0x0, 'Array[2] should be 0'); } #[test] @@ -63,8 +56,8 @@ fn test_storage_set_many() { values.append(0x3); values.append(0x4); - storage::set_many(0, keys.span(), 0, values.span(), array![251, 251, 251, 251].span()); - let many = storage::get_many(0, keys.span(), 0, 4, array![251, 251, 251, 251].span()); + storage::set_many(0, keys.span(), values.span(), array![251, 251, 251, 251].span()).unwrap(); + let many = storage::get_many(0, keys.span(), array![251, 251, 251, 251].span()).unwrap(); assert(many.at(0) == values.at(0), 'Value at 0 not equal!'); assert(many.at(1) == values.at(1), 'Value at 1 not equal!'); assert(many.at(2) == values.at(2), 'Value at 2 not equal!'); @@ -72,22 +65,36 @@ fn test_storage_set_many() { } #[test] -#[available_gas(20000000)] -fn test_storage_set_many_with_offset() { +#[available_gas(2000000000)] +fn test_storage_set_many_several_segments() { let mut keys = ArrayTrait::new(); - keys.append(0x1364); + keys.append(0x966); + let mut layout = ArrayTrait::new(); let mut values = ArrayTrait::new(); - values.append(0x1); - values.append(0x2); - values.append(0x3); - values.append(0x4); + let mut i = 0; + loop { + if i == 1000 { + break; + } + + values.append(i); + layout.append(251_u8); + + i += 1; + }; - storage::set_many(0, keys.span(), 1, values.span(), array![251, 251, 251, 251].span()); - let many = storage::get_many(0, keys.span(), 0, 5, array![251, 251, 251, 251, 251].span()); - assert(*many.at(0) == 0x0, 'Value at 0 not equal!'); - assert(many.at(1) == values.at(0), 'Value at 1 not equal!'); - assert(many.at(2) == values.at(1), 'Value at 2 not equal!'); - assert(many.at(3) == values.at(2), 'Value at 3 not equal!'); - assert(many.at(4) == values.at(3), 'Value at 4 not equal!'); + storage::set_many(0, keys.span(), values.span(), layout.span()).unwrap(); + let many = storage::get_many(0, keys.span(), layout.span()).unwrap(); + + let mut i = 0; + loop { + if i == 1000 { + break; + } + + assert(many.at(i) == values.at(i), 'Value not equal!'); + + i += 1; + }; } diff --git a/crates/dojo-core/src/database/utils.cairo b/crates/dojo-core/src/database/utils.cairo deleted file mode 100644 index 62ae3ff531..0000000000 --- a/crates/dojo-core/src/database/utils.cairo +++ /dev/null @@ -1,166 +0,0 @@ -use array::{ArrayTrait, SpanTrait}; -use option::OptionTrait; -use traits::{Default, Into, TryInto}; -use dict::Felt252DictTrait; - -// big enough number used to construct a compound key in `find_matching` -const OFFSET: felt252 = 0x10000000000000000000000000000000000; - -// finds only those entities that have same IDs across all provided entities and -// returns these entities, obeying the order of IDs of the first ID array -// -// the function takes two aruments: -// * `ids` is a list of lists of entity IDs; each inner list is an ID of an entity -// at the same index in the corresponding `entities` list -// * `entities` is a list of lists of deserialized entities; each list of entities -// is of the same entity type in the order of IDs from `ids -// -// to illustrate, consider we have two entity types (models), Place and Owner -// `ids` are [[4, 2, 3], [3, 4, 5]] -// `entities` are [[P4, P2, P3], [O3, O4, O5]] -// where P4 is a deserialized (i.e. a Span) Place entity with ID 4, -// O3 is a deserialized Owner entity with ID 4 and so on.. -// -// the function would return [[P4, P3], [O4, O3]] because IDs 3 and 4 are found -// for all entities and the function respects the ID order from the first ID array, -// hence 4 and 3 in this case -fn find_matching( - mut ids: Span>, mut entities: Span>> -) -> Span>> { - assert(ids.len() == entities.len(), 'lengths dont match'); - - let entity_types_count = entities.len(); - if entity_types_count == 1 { - return entities; - } - - // keeps track of how many times has an ID been encountered - let mut ids_match: Felt252Dict = Default::default(); - - // keeps track of indexes where a particular entity with ID is in - // each entity array; to do so, we're using a compound key or 2 parts - // first part is the entity *type* (calculated as OFFSET * entity_type_counter) - // second part is the entity ID itself - let mut id_to_idx: Felt252Dict = Default::default(); - - // how many ID arrays have we looped over so far; ultimately - // this number is the same as ids.len() and we use only those - // IDs from ids_match where the value is the same as match_count - - // we want to keep the ordering from the first entity IDs - let mut ids1: Span = *(ids.pop_front().unwrap()); - - // counts how many ID arrays and hence entity types we've looped over - // starts at 1 because we skip the first element to keep ordering (see above) - let mut entity_type_counter: u8 = 1; - - loop { - // loop through the rest of the IDs for entity types 2..N - match ids.pop_front() { - Option::Some(entity_ids) => { - let mut index: usize = 0; - let mut entity_ids = *entity_ids; - - loop { - // loop through each ID of an entity type - match entity_ids.pop_front() { - Option::Some(id) => { - // keep track how many times we've encountered a particular ID - let c = ids_match[*id]; - ids_match.insert(*id, c + 1); - // keep track of the index of the particular entity in an - // entity type array, i.e. at which index is the entity - // with `id` at, using the compound key - id_to_idx.insert(OFFSET * entity_type_counter.into() + *id, index); - index += 1; - }, - Option::None(_) => { - break (); - } - }; - }; - - entity_type_counter += 1; - }, - Option::None(_) => { - break (); - } - }; - }; - - let first_entities: Span> = *entities[0]; - let mut first_entities_idx = 0; - - // an array into which we append those entities who's IDs are found across - // every ID array; the entities are appended sequentially, e.g. - // [entity1_id1, entity2_id1, entity3_id1, entity1_id2, entity2_id2, entity3_id2] - // perserving the ID order from the first ID array - let mut entities_with_matching_ids: Array> = ArrayTrait::new(); - - let found_in_all: u8 = entity_type_counter - 1; - - loop { - match ids1.pop_front() { - Option::Some(id) => { - let id = *id; - if ids_match[id] == found_in_all { - // id was found in every entity_ids array - - // append the matching entity to the array - entities_with_matching_ids.append(*first_entities[first_entities_idx]); - - // now append all the other matching entities there too - let mut entity_types_idx = 1; - loop { - if entity_types_idx == entity_types_count { - break (); - } - let idx_for_matching_id = id_to_idx[OFFSET * entity_types_idx.into() + id]; - let same_type_entities = entities[entity_types_idx]; - entities_with_matching_ids.append(*same_type_entities[idx_for_matching_id]); - - entity_types_idx += 1; - } - } - first_entities_idx += 1; - }, - Option::None(_) => { - break (); - } - }; - }; - - ids_match.squash(); - id_to_idx.squash(); - - let mut entities_with_matching_ids = entities_with_matching_ids.span(); - // calculate how many common IDs across all entities we found - // guaranteed to be a round number - let matches = entities_with_matching_ids.len() / entity_types_count; - let mut result: Array>> = ArrayTrait::new(); - let mut i = 0; - - // finally, reorder the entities from the temporary array - // into the resulting one in a way where they are grouped together by - // entity type - loop { - if i == entity_types_count { - break (); - } - - let mut j = 0; - let mut same_entities: Array> = ArrayTrait::new(); - loop { - if j == matches { - break (); - } - same_entities.append(*entities_with_matching_ids[j * entity_types_count + i]); - j += 1; - }; - - result.append(same_entities.span()); - i += 1; - }; - - result.span() -} diff --git a/crates/dojo-core/src/database/utils_test.cairo b/crates/dojo-core/src/database/utils_test.cairo deleted file mode 100644 index fc7031ab2c..0000000000 --- a/crates/dojo-core/src/database/utils_test.cairo +++ /dev/null @@ -1,144 +0,0 @@ -use array::{ArrayTrait, SpanTrait}; -use option::OptionTrait; -use traits::Into; - -use dojo::database::utils::find_matching; - -fn build_fake_entity(v: felt252) -> Span { - let mut e = ArrayTrait::new(); - e.append(v); - e.append(v); - e.append(v); - e.span() -} - -fn assert_entity(entity: Span, v: felt252) { - assert(entity.len() == 3, 'entity len'); - assert(*entity[0] == v, 'entity 0'); - assert(*entity[1] == v, 'entity 1'); - assert(*entity[2] == v, 'entity 2'); -} - -#[test] -#[available_gas(1000000000)] -fn test_find_matching() { - let mut ids1: Array = ArrayTrait::new(); - let mut ids2: Array = ArrayTrait::new(); - let mut ids3: Array = ArrayTrait::new(); - - ids1.append(1); - ids1.append(3); - ids1.append(6); - ids1.append(5); - - ids2.append(4); - ids2.append(5); - ids2.append(3); - - ids3.append(3); - ids3.append(2); - ids3.append(1); - ids3.append(7); - ids3.append(5); - - let mut ids: Array> = ArrayTrait::new(); - ids.append(ids1.span()); - ids.append(ids2.span()); - ids.append(ids3.span()); - - let mut e1: Array> = ArrayTrait::new(); - e1.append(build_fake_entity(1)); - e1.append(build_fake_entity(3)); - e1.append(build_fake_entity(6)); - e1.append(build_fake_entity(5)); - - let mut e2: Array> = ArrayTrait::new(); - e2.append(build_fake_entity(40)); - e2.append(build_fake_entity(50)); - e2.append(build_fake_entity(30)); - - let mut e3: Array> = ArrayTrait::new(); - e3.append(build_fake_entity(300)); - e3.append(build_fake_entity(200)); - e3.append(build_fake_entity(100)); - e3.append(build_fake_entity(700)); - e3.append(build_fake_entity(500)); - - let mut entities: Array>> = ArrayTrait::new(); - entities.append(e1.span()); - entities.append(e2.span()); - entities.append(e3.span()); - - let matching = find_matching(ids.span(), entities.span()); - - // there is a match only on entities with IDs 3 and 5 - // and matching should look like: - // [ - // [[3, 3, 3], [5, 5, 5]], - // [[30, 30, 30], [50, 50, 50]], - // [[300, 300, 300], [500, 500, 500]] - // ] - - assert(matching.len() == 3, 'matching len'); - - let entities0 = *matching[0]; - assert(entities0.len() == 2, 'entities0 len'); - assert_entity(*entities0[0], 3); - assert_entity(*entities0[1], 5); - - let entities1 = *matching[1]; - assert(entities1.len() == 2, 'entities1 len'); - assert_entity(*entities1[0], 30); - assert_entity(*entities1[1], 50); - - let entities2 = *matching[2]; - assert(entities2.len() == 2, 'entities2 len'); - assert_entity(*entities2[0], 300); - assert_entity(*entities2[1], 500); -} - -#[test] -#[available_gas(1000000000)] -#[should_panic(expected: ('lengths dont match', ))] -fn test_find_matching_wrong_arg_len() { - let mut ids1: Array = ArrayTrait::new(); - let mut ids2: Array = ArrayTrait::new(); - - ids1.append(1); - ids1.append(3); - ids1.append(6); - ids1.append(5); - - ids2.append(4); - ids2.append(5); - ids2.append(3); - - let mut ids: Array> = ArrayTrait::new(); - ids.append(ids1.span()); - ids.append(ids2.span()); - - let mut e1: Array> = ArrayTrait::new(); - e1.append(build_fake_entity(1)); - e1.append(build_fake_entity(3)); - e1.append(build_fake_entity(6)); - e1.append(build_fake_entity(5)); - - let mut e2: Array> = ArrayTrait::new(); - e2.append(build_fake_entity(40)); - e2.append(build_fake_entity(50)); - e2.append(build_fake_entity(30)); - - let mut e3: Array> = ArrayTrait::new(); - e3.append(build_fake_entity(300)); - e3.append(build_fake_entity(200)); - e3.append(build_fake_entity(100)); - e3.append(build_fake_entity(700)); - e3.append(build_fake_entity(500)); - - let mut entities: Array>> = ArrayTrait::new(); - entities.append(e1.span()); - entities.append(e2.span()); - entities.append(e3.span()); - - let _matching = find_matching(ids.span(), entities.span()); -} diff --git a/crates/dojo-core/src/database_test.cairo b/crates/dojo-core/src/database_test.cairo index c1f760bb06..f61f9338f8 100644 --- a/crates/dojo-core/src/database_test.cairo +++ b/crates/dojo-core/src/database_test.cairo @@ -8,9 +8,7 @@ use traits::{Into, TryInto}; use starknet::syscalls::deploy_syscall; use starknet::class_hash::{Felt252TryIntoClassHash, ClassHash}; use dojo::world::{IWorldDispatcher}; -use dojo::executor::executor; -use dojo::database::{get, set, set_with_index, del, scan}; -use dojo::database::index::WhereCondition; +use dojo::database::{get, set}; #[test] #[available_gas(1000000)] @@ -19,8 +17,8 @@ fn test_database_basic() { values.append('database_test'); values.append('42'); - set('table', 'key', 0, values.span(), array![251, 251].span()); - let res = get('table', 'key', 0, values.len(), array![251, 251].span()); + set('table', 'key', values.span(), array![251, 251].span()); + let res = get('table', 'key', array![251, 251].span()); assert(res.at(0) == values.at(0), 'Value at 0 not equal!'); assert(res.at(1) == values.at(1), 'Value at 0 not equal!'); @@ -38,10 +36,10 @@ fn test_database_different_tables() { other.append(0x3); other.append(0x4); - set('first', 'key', 0, values.span(), array![251, 251].span()); - set('second', 'key', 0, other.span(), array![251, 251].span()); - let res = get('first', 'key', 0, values.len(), array![251, 251].span()); - let other_res = get('second', 'key', 0, other.len(), array![251, 251].span()); + set('first', 'key', values.span(), array![251, 251].span()); + set('second', 'key', other.span(), array![251, 251].span()); + let res = get('first', 'key', array![251, 251].span()); + let other_res = get('second', 'key', array![251, 251].span()); assert(res.len() == values.len(), 'Lengths not equal'); assert(res.at(0) == values.at(0), 'Values different at `first`!'); @@ -60,70 +58,13 @@ fn test_database_different_keys() { other.append(0x3); other.append(0x4); - set('table', 'key', 0, values.span(), array![251, 251].span()); - set('table', 'other', 0, other.span(), array![251, 251].span()); - let res = get('table', 'key', 0, values.len(), array![251, 251].span()); - let other_res = get('table', 'other', 0, other.len(), array![251, 251].span()); + set('table', 'key', values.span(), array![251, 251].span()); + set('table', 'other', other.span(), array![251, 251].span()); + let res = get('table', 'key', array![251, 251].span()); + let other_res = get('table', 'other', array![251, 251].span()); assert(res.len() == values.len(), 'Lengths not equal'); assert(res.at(0) == values.at(0), 'Values different at `key`!'); assert(other_res.at(0) == other_res.at(0), 'Values different at `other`!'); assert(other_res.at(0) != res.at(0), 'Values the same for different!'); } - -#[test] -#[available_gas(10000000)] -fn test_database_pagination() { - let mut values = ArrayTrait::new(); - values.append(0x1); - values.append(0x2); - values.append(0x3); - values.append(0x4); - values.append(0x5); - - set('table', 'key', 1, values.span(), array![251, 251, 251, 251, 251].span()); - let first_res = get('table', 'key', 1, 3, array![251, 251, 251].span()); - let second_res = get('table', 'key', 3, 5, array![251, 251, 251, 251, 251].span()); - let third_res = get('table', 'key', 5, 7, array![251, 251, 251, 251, 251, 251, 251].span()); - - assert(*first_res.at(0) == *values.at(0), 'Values different at index 0!'); - assert(*first_res.at(1) == *values.at(1), 'Values different at index 1!'); - assert(*second_res.at(0) == *values.at(2), 'Values different at index 2!'); - assert(*second_res.at(1) == *values.at(3), 'Values different at index 3!'); - assert(*third_res.at(0) == *values.at(4), 'Values different at index 4!'); - assert(*third_res.at(1) == 0x0, 'Value not empty at index 5!'); -} - -#[test] -#[available_gas(10000000)] -fn test_database_del() { - let mut values = ArrayTrait::new(); - values.append(0x42); - - set('table', 'key', 0, values.span(), array![251].span()); - - let before = get('table', 'key', 0, values.len(), array![251].span()); - assert(*before.at(0) == *values.at(0), 'Values different at index 0!'); - - del('table', 'key'); - let after = get('table', 'key', 0, 0, array![].span()); - assert(after.len() == 0, 'Non empty after deletion!'); -} - -#[test] -#[available_gas(10000000)] -fn test_database_scan() { - let even = array![2, 4].span(); - let odd = array![1, 3].span(); - let layout = array![251, 251].span(); - - set_with_index('table', 'even', 0, even, layout); - set_with_index('table', 'odd', 0, odd, layout); - - let (keys, values) = scan('table', Option::None(()), 2, layout); - assert(keys.len() == 2, 'Wrong number of keys!'); - assert(values.len() == 2, 'Wrong number of values!'); - assert(*keys.at(0) == 'even', 'Wrong key at index 0!'); - assert(*(*values.at(0)).at(0) == 2, 'Wrong value at index 0!'); - assert(*(*values.at(0)).at(1) == 4, 'Wrong value at index 1!'); -} diff --git a/crates/dojo-core/src/executor.cairo b/crates/dojo-core/src/executor.cairo deleted file mode 100644 index 5d824f23e8..0000000000 --- a/crates/dojo-core/src/executor.cairo +++ /dev/null @@ -1,47 +0,0 @@ -use starknet::ClassHash; - -#[starknet::interface] -trait IExecutor { - fn call( - self: @T, class_hash: ClassHash, entrypoint: felt252, calldata: Span - ) -> Span; -} - -#[starknet::contract] -mod executor { - use array::{ArrayTrait, SpanTrait}; - use option::OptionTrait; - use starknet::{ClassHash, SyscallResultTrait, SyscallResultTraitImpl}; - - use super::IExecutor; - - const EXECUTE_ENTRYPOINT: felt252 = - 0x0240060cdb34fcc260f41eac7474ee1d7c80b7e3607daff9ac67c7ea2ebb1c44; - - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl Executor of IExecutor { - /// Call the provided `entrypoint` method on the given `class_hash`. - /// - /// # Arguments - /// - /// * `class_hash` - Class Hash to call. - /// * `entrypoint` - Entrypoint to call. - /// * `calldata` - The calldata to pass. - /// - /// # Returns - /// - /// The return value of the call. - fn call( - self: @ContractState, - class_hash: ClassHash, - entrypoint: felt252, - calldata: Span - ) -> Span { - starknet::syscalls::library_call_syscall(class_hash, entrypoint, calldata) - .unwrap_syscall() - } - } -} diff --git a/crates/dojo-core/src/executor_test.cairo b/crates/dojo-core/src/executor_test.cairo deleted file mode 100644 index 109a1b8f5e..0000000000 --- a/crates/dojo-core/src/executor_test.cairo +++ /dev/null @@ -1,69 +0,0 @@ -use core::traits::Into; -use core::result::ResultTrait; -use array::ArrayTrait; -use option::OptionTrait; -use serde::Serde; -use traits::TryInto; - -use starknet::syscalls::deploy_syscall; -use starknet::class_hash::Felt252TryIntoClassHash; -use dojo::executor::{executor, IExecutorDispatcher, IExecutorDispatcherTrait}; -use dojo::world::{IWorldDispatcher}; - -#[derive(Model, Copy, Drop, Serde)] -struct Foo { - #[key] - id: felt252, - a: felt252, - b: u128, -} - -#[starknet::interface] -trait IBar { - fn dojo_resource(self: @T) -> felt252; - fn execute(self: @T, foo: Foo) -> Foo; -} - -#[starknet::contract] -mod bar { - use super::{Foo, IBar}; - - #[storage] - struct Storage {} - - #[abi(embed_v0)] - impl IbarImpl of IBar { - fn dojo_resource(self: @ContractState) -> felt252 { - 'bar' - } - - fn execute(self: @ContractState, foo: Foo) -> Foo { - foo - } - } -} - -const DOJO_RESOURCE_ENTRYPOINT: felt252 = - 0x038f2d91dabc7079b6f336cc00f874d17cbb7463674c7d3edfd04668fbdb6f6a; - - -#[test] -#[available_gas(40000000)] -fn test_executor() { - let constructor_calldata = array::ArrayTrait::new(); - let (executor_address, _) = deploy_syscall( - executor::TEST_CLASS_HASH.try_into().unwrap(), 0, constructor_calldata.span(), false - ) - .unwrap(); - - let executor = IExecutorDispatcher { contract_address: executor_address }; - - starknet::testing::set_contract_address(starknet::contract_address_const::<0x1337>()); - - let res = *executor - .call( - bar::TEST_CLASS_HASH.try_into().unwrap(), DOJO_RESOURCE_ENTRYPOINT, array![].span() - )[0]; - - assert(res == 'bar', 'executor call incorrect') -} diff --git a/crates/dojo-core/src/lib.cairo b/crates/dojo-core/src/lib.cairo index 6f4abe6691..33dc960fa7 100644 --- a/crates/dojo-core/src/lib.cairo +++ b/crates/dojo-core/src/lib.cairo @@ -4,9 +4,6 @@ mod base_test; mod database; #[cfg(test)] mod database_test; -mod executor; -#[cfg(test)] -mod executor_test; mod model; mod packing; #[cfg(test)] @@ -22,3 +19,4 @@ mod test_utils; mod benchmarks; mod components; +mod resource_metadata; diff --git a/crates/dojo-core/src/model.cairo b/crates/dojo-core/src/model.cairo index 1547c2b340..312fc3c32c 100644 --- a/crates/dojo-core/src/model.cairo +++ b/crates/dojo-core/src/model.cairo @@ -1,3 +1,5 @@ +use starknet::SyscallResult; + trait Model { fn name(self: @T) -> felt252; fn keys(self: @T) -> Span; @@ -13,7 +15,6 @@ trait IModel { fn schema(self: @T) -> Span; } - #[starknet::interface] trait IDojoModel { fn name(self: @T) -> felt252; @@ -21,4 +22,27 @@ trait IDojoModel { fn packed_size(self: @T) -> usize; fn layout(self: @T) -> Span; fn schema(self: @T) -> dojo::database::introspect::Ty; -} \ No newline at end of file +} + +/// Deploys a model with the given [`ClassHash`] and retrieves it's name. +/// Currently, the model is expected to already be declared by `sozo`. +/// +/// # Arguments +/// +/// * `class_hash` - Class Hash of the model. +fn deploy_and_get_name(salt: felt252, class_hash: starknet::ClassHash) -> SyscallResult<(starknet::ContractAddress, felt252)> { + let (address, _) = starknet::deploy_syscall( + class_hash, + salt, + array![].span(), + false, + )?; + + let name = *starknet::call_contract_syscall( + address, + selector!("name"), + array![].span() + )?[0]; + + Result::Ok((address, name)) +} diff --git a/crates/dojo-core/src/packing.cairo b/crates/dojo-core/src/packing.cairo index 2d7bff76a6..0ae853adcc 100644 --- a/crates/dojo-core/src/packing.cairo +++ b/crates/dojo-core/src/packing.cairo @@ -4,6 +4,8 @@ use traits::{Into, TryInto}; use integer::{U256BitAnd, U256BitOr, U256BitXor, upcast, downcast, BoundedInt}; use option::OptionTrait; +const PACKING_MAX_BITS: u8 = 251; + fn pack(ref packed: Array, ref unpacked: Span, ref layout: Span) { assert(unpacked.len() == layout.len(), 'mismatched input lens'); let mut packing: felt252 = 0x0; @@ -30,7 +32,7 @@ fn calculate_packed_size(ref layout: Span) -> usize { Option::Some(item) => { let item_size: usize = (*item).into(); partial += item_size; - if (partial > 251) { + if (partial > PACKING_MAX_BITS.into()) { size += 1; partial = item_size; } @@ -46,7 +48,7 @@ fn calculate_packed_size(ref layout: Span) -> usize { fn unpack(ref unpacked: Array, ref packed: Span, ref layout: Span) { let mut unpacking: felt252 = 0x0; - let mut offset: u8 = 251; + let mut offset: u8 = PACKING_MAX_BITS; loop { match layout.pop_front() { Option::Some(s) => { @@ -55,8 +57,9 @@ fn unpack(ref unpacked: Array, ref packed: Span, ref layout: S unpacked.append(u); }, Option::None(_) => { - // TODO: Raise error - break; + // Layout value was successfully poped, + // we are then expecting an unpacked value. + panic_with_felt252('Unpack inner failed'); } } }, @@ -75,10 +78,13 @@ fn pack_inner( ref packing_offset: u8, ref packed: Array ) { + assert(packing_offset <= PACKING_MAX_BITS, 'Invalid packing offset'); + assert(size <= PACKING_MAX_BITS, 'Invalid layout size'); + // Cannot use all 252 bits because some bit arrangements (eg. 11111...11111) are not valid felt252 values. // Thus only 251 bits are used. ^-252 times-^ // One could optimize by some conditional alligment mechanism, but it would be an at most 1/252 space-wise improvement. - let remaining_bits: u8 = (251 - packing_offset).into(); + let remaining_bits: u8 = (PACKING_MAX_BITS - packing_offset).into(); // If we have less remaining bits than the current item size, // Finalize the current `packing`felt and move to the next felt. @@ -102,7 +108,7 @@ fn pack_inner( fn unpack_inner( size: u8, ref packed: Span, ref unpacking: felt252, ref unpacking_offset: u8 ) -> Option { - let remaining_bits: u8 = (251 - unpacking_offset).into(); + let remaining_bits: u8 = (PACKING_MAX_BITS - unpacking_offset).into(); // If less remaining bits than size, we move to the next // felt for unpacking. @@ -113,7 +119,7 @@ fn unpack_inner( unpacking_offset = size; // If we are unpacking a full felt. - if (size == 251) { + if (size == PACKING_MAX_BITS) { return Option::Some(unpacking); } @@ -134,6 +140,10 @@ fn unpack_inner( } fn fpow(x: u256, n: u8) -> u256 { + if x.is_zero() { + panic_with_felt252('base 0 not allowed in fpow'); + } + let y = x; if n == 0 { return 1; @@ -154,4 +164,4 @@ fn shl(x: u256, n: u8) -> u256 { fn shr(x: u256, n: u8) -> u256 { x / fpow(2, n) -} \ No newline at end of file +} diff --git a/crates/dojo-core/src/packing_test.cairo b/crates/dojo-core/src/packing_test.cairo index 5ad14bb392..12d817de2c 100644 --- a/crates/dojo-core/src/packing_test.cairo +++ b/crates/dojo-core/src/packing_test.cairo @@ -56,7 +56,7 @@ fn test_pack_unpack_felt252_u128() { let mut packing: felt252 = 0; let mut offset = 0; pack_inner(@1337, 128, ref packing, ref offset, ref packed); - pack_inner(@420, 252, ref packing, ref offset, ref packed); + pack_inner(@420, 251, ref packing, ref offset, ref packed); packed.append(packing); let mut unpacking: felt252 = packed.pop_front().unwrap(); @@ -353,3 +353,17 @@ fn test_calculate_packed_size() { let got = calculate_packed_size(ref layout); assert(got == 2, 'invalid length'); } + +#[test] +#[available_gas(9000000)] +#[should_panic(expected: ('Invalid layout size',))] +fn test_pack_max_bits_value() { + let mut unpacked = array![1]; + let mut layout = array![253]; + + let mut layout_span = layout.span(); + let mut unpacked_span = unpacked.span(); + + let mut packed = array![]; + pack(ref packed, ref unpacked_span, ref layout_span); +} diff --git a/crates/dojo-core/src/resource_metadata.cairo b/crates/dojo-core/src/resource_metadata.cairo new file mode 100644 index 0000000000..9c39d11be1 --- /dev/null +++ b/crates/dojo-core/src/resource_metadata.cairo @@ -0,0 +1,133 @@ +//! ResourceMetadata model. +//! +//! Manually expand to ensure that dojo-core +//! does not depend on dojo plugin to be built. +//! +const RESOURCE_METADATA_MODEL: felt252 = 'ResourceMetadata'; + +fn initial_address() -> starknet::ContractAddress { + starknet::contract_address_const::<0>() +} + +fn initial_class_hash() -> starknet::ClassHash { + starknet::class_hash_const::<0x03f75587469e8101729b3b02a46150a3d99315bc9c5026d64f2e8a061e413255>() +} + +#[derive(Drop, Serde, PartialEq, Clone)] +struct ResourceMetadata { + // #[key] + resource_id: felt252, + // #[capacity(3)]. + metadata_uri: Span, +} + +impl ResourceMetadataModel of dojo::model::Model { + #[inline(always)] + fn name(self: @ResourceMetadata) -> felt252 { + 'ResourceMetadata' + } + + #[inline(always)] + fn keys(self: @ResourceMetadata) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + core::array::ArrayTrait::append(ref serialized, *self.resource_id); + core::array::ArrayTrait::span(@serialized) + } + + #[inline(always)] + fn values(self: @ResourceMetadata) -> Span { + let mut serialized = core::array::ArrayTrait::new(); + core::serde::Serde::serialize(self.metadata_uri, ref serialized); + core::array::ArrayTrait::span(@serialized) + } + + #[inline(always)] + fn layout(self: @ResourceMetadata) -> Span { + let mut layout = core::array::ArrayTrait::new(); + dojo::database::introspect::Introspect::::layout(ref layout); + core::array::ArrayTrait::span(@layout) + } + + #[inline(always)] + fn packed_size(self: @ResourceMetadata) -> usize { + let mut layout = self.layout(); + dojo::packing::calculate_packed_size(ref layout) + } +} + +impl ResourceMetadataIntrospect<> of dojo::database::introspect::Introspect> { + #[inline(always)] + fn size() -> usize { + // Length of array first + capacity. + 1 + 3 + } + + #[inline(always)] + fn layout(ref layout: Array) { + // Len of array first. + layout.append(251); + // Capacity. + layout.append(251); + layout.append(251); + layout.append(251); + } + + #[inline(always)] + fn ty() -> dojo::database::introspect::Ty { + dojo::database::introspect::Ty::Struct(dojo::database::introspect::Struct { + name: 'ResourceMetadata', + attrs: array![].span(), + children: array![dojo::database::introspect::serialize_member(@dojo::database::introspect::Member { + name: 'resource_id', + ty: dojo::database::introspect::Ty::Primitive('felt252'), + attrs: array!['key'].span() + }), dojo::database::introspect::serialize_member(@dojo::database::introspect::Member { + name: 'metadata_uri', + ty: dojo::database::introspect::Ty::Array(3), + attrs: array![].span() + })].span() + }) + } +} + +#[starknet::contract] +mod resource_metadata { + use super::ResourceMetadata; + + #[storage] + struct Storage {} + + #[external(v0)] + fn name(self: @ContractState) -> felt252 { + 'ResourceMetadata' + } + + #[external(v0)] + fn unpacked_size(self: @ContractState) -> usize { + dojo::database::introspect::Introspect::::size() + } + + #[external(v0)] + fn packed_size(self: @ContractState) -> usize { + let mut layout = core::array::ArrayTrait::new(); + dojo::database::introspect::Introspect::::layout(ref layout); + let mut layout_span = layout.span(); + dojo::packing::calculate_packed_size(ref layout_span) + } + + #[external(v0)] + fn layout(self: @ContractState) -> Span { + let mut layout = core::array::ArrayTrait::new(); + dojo::database::introspect::Introspect::::layout(ref layout); + core::array::ArrayTrait::span(@layout) + } + + #[external(v0)] + fn schema(self: @ContractState) -> dojo::database::introspect::Ty { + dojo::database::introspect::Introspect::::ty() + } + + #[external(v0)] + fn ensure_abi(self: @ContractState, model: ResourceMetadata) { + } +} diff --git a/crates/dojo-core/src/test_utils.cairo b/crates/dojo-core/src/test_utils.cairo index ea9bec6216..6ffbedb98e 100644 --- a/crates/dojo-core/src/test_utils.cairo +++ b/crates/dojo-core/src/test_utils.cairo @@ -8,9 +8,9 @@ use option::OptionTrait; use core::{result::ResultTrait, traits::Into}; use debug::PrintTrait; -use dojo::executor::executor; use dojo::world::{world, IWorldDispatcher, IWorldDispatcherTrait}; use dojo::packing::{shl, shr}; +use dojo::resource_metadata::resource_metadata; /// Deploy classhash with calldata for constructor /// @@ -43,22 +43,20 @@ fn deploy_with_world_address(class_hash: felt252, world: IWorldDispatcher) -> Co fn spawn_test_world(models: Array) -> IWorldDispatcher { let salt = testing::get_available_gas(); - // deploy executor - let constructor_calldata = array::ArrayTrait::new(); - let (executor_address, _) = deploy_syscall( - executor::TEST_CLASS_HASH.try_into().unwrap(), salt.into(), constructor_calldata.span(), true - ) - .unwrap(); // deploy world let (world_address, _) = deploy_syscall( world::TEST_CLASS_HASH.try_into().unwrap(), salt.into(), - array![executor_address.into(), dojo::base::base::TEST_CLASS_HASH].span(), - true + array![dojo::base::base::TEST_CLASS_HASH].span(), + false ) .unwrap(); + let world = IWorldDispatcher { contract_address: world_address }; + // Register the resource metadata. + world.register_model(resource_metadata::TEST_CLASS_HASH.try_into().unwrap()); + // register models let mut index = 0; loop { diff --git a/crates/dojo-core/src/world.cairo b/crates/dojo-core/src/world.cairo index 48832d0e39..0abb895426 100644 --- a/crates/dojo-core/src/world.cairo +++ b/crates/dojo-core/src/world.cairo @@ -1,39 +1,28 @@ use starknet::{ContractAddress, ClassHash, StorageBaseAddress, SyscallResult}; use traits::{Into, TryInto}; use option::OptionTrait; +use dojo::resource_metadata::ResourceMetadata; #[starknet::interface] trait IWorld { - fn metadata_uri(self: @T, resource: felt252) -> Span; - fn set_metadata_uri(ref self: T, resource: felt252, uri: Span); - fn model(self: @T, name: felt252) -> ClassHash; + fn metadata(self: @T, resource_id: felt252) -> ResourceMetadata; + fn set_metadata(ref self: T, metadata: ResourceMetadata); + fn model(self: @T, name: felt252) -> (ClassHash, ContractAddress); fn register_model(ref self: T, class_hash: ClassHash); fn deploy_contract(ref self: T, salt: felt252, class_hash: ClassHash) -> ContractAddress; fn upgrade_contract(ref self: T, address: ContractAddress, class_hash: ClassHash) -> ClassHash; fn uuid(ref self: T) -> usize; fn emit(self: @T, keys: Array, values: Span); fn entity( - self: @T, model: felt252, keys: Span, offset: u8, length: usize, layout: Span + self: @T, model: felt252, keys: Span, layout: Span ) -> Span; fn set_entity( ref self: T, model: felt252, keys: Span, - offset: u8, values: Span, layout: Span ); - fn entities( - self: @T, - model: felt252, - index: Option, - values: Span, - values_length: usize, - values_layout: Span - ) -> (Span, Span>); - fn entity_ids(self: @T, model: felt252) -> Span; - fn set_executor(ref self: T, contract_address: ContractAddress); - fn executor(self: @T) -> ContractAddress; fn base(self: @T) -> ClassHash; fn delete_entity(ref self: T, model: felt252, keys: Span, layout: Span); fn is_owner(self: @T, address: ContractAddress, resource: felt252) -> bool; @@ -60,6 +49,15 @@ trait IDojoResourceProvider { fn dojo_resource(self: @T) -> felt252; } +mod Errors { + const METADATA_DESER: felt252 = 'metadata deser error'; + const NOT_OWNER: felt252 = 'not owner'; + const NOT_OWNER_WRITER: felt252 = 'not owner or writer'; + const INVALID_MODEL_NAME: felt252 = 'invalid model name'; + const OWNER_ONLY_UPGRADE: felt252 = 'only owner can upgrade'; + const OWNER_ONLY_UPDATE: felt252 = 'only owner can update'; +} + #[starknet::contract] mod world { use core::traits::TryInto; @@ -79,14 +77,14 @@ mod world { }; use dojo::database; - use dojo::database::index::WhereCondition; - use dojo::executor::{IExecutorDispatcher, IExecutorDispatcherTrait}; - use dojo::world::{IWorldDispatcher, IWorld, IUpgradeableWorld}; - + use dojo::database::introspect::Introspect; use dojo::components::upgradeable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; + use dojo::model::Model; + use dojo::world::{IWorldDispatcher, IWorld, IUpgradeableWorld}; + use dojo::resource_metadata; + use dojo::resource_metadata::{ResourceMetadata, RESOURCE_METADATA_MODEL}; - const NAME_ENTRYPOINT: felt252 = - 0x0361458367e696363fbcc70777d07ebbd2394e89fd0adcaf147faccd1d294d60; + use super::Errors; const WORLD: felt252 = 0; @@ -103,7 +101,6 @@ mod world { StoreDelRecord: StoreDelRecord, WriterUpdated: WriterUpdated, OwnerUpdated: OwnerUpdated, - ExecutorUpdated: ExecutorUpdated } #[derive(Drop, starknet::Event)] @@ -140,14 +137,15 @@ mod world { struct ModelRegistered { name: felt252, class_hash: ClassHash, - prev_class_hash: ClassHash + prev_class_hash: ClassHash, + address: ContractAddress, + prev_address: ContractAddress, } #[derive(Drop, starknet::Event)] struct StoreSetRecord { table: felt252, keys: Span, - offset: u8, values: Span, } @@ -171,106 +169,72 @@ mod world { value: bool, } - #[derive(Drop, starknet::Event)] - struct ExecutorUpdated { - address: ContractAddress, - prev_address: ContractAddress, - } - - #[storage] struct Storage { - executor_dispatcher: IExecutorDispatcher, contract_base: ClassHash, nonce: usize, - metadata_uri: LegacyMap::, - models: LegacyMap::, + models_count: usize, + models: LegacyMap::, + deployed_contracts: LegacyMap::, owners: LegacyMap::<(felt252, ContractAddress), bool>, writers: LegacyMap::<(felt252, ContractAddress), bool>, } #[constructor] - fn constructor(ref self: ContractState, executor: ContractAddress, contract_base: ClassHash) { + fn constructor(ref self: ContractState, contract_base: ClassHash) { let creator = starknet::get_tx_info().unbox().account_contract_address; - self.executor_dispatcher.write(IExecutorDispatcher { contract_address: executor }); self.contract_base.write(contract_base); self.owners.write((WORLD, creator), true); - EventEmitter::emit(ref self, WorldSpawned { address: get_contract_address(), creator }); - } + // Ensure the creator of the world is the owner of the resource metadata model. + self.owners.write((RESOURCE_METADATA_MODEL, creator), true); + self.models.write( + RESOURCE_METADATA_MODEL, + (resource_metadata::initial_class_hash(), resource_metadata::initial_address()) + ); - /// Call Helper, - /// Call the provided `entrypoint` method on the given `class_hash`. - /// - /// # Arguments - /// - /// * `class_hash` - Class Hash to call. - /// * `entrypoint` - Entrypoint to call. - /// * `calldata` - The calldata to pass. - /// - /// # Returns - /// - /// The return value of the call. - fn class_call( - self: @ContractState, class_hash: ClassHash, entrypoint: felt252, calldata: Span - ) -> Span { - self.executor_dispatcher.read().call(class_hash, entrypoint, calldata) + EventEmitter::emit(ref self, WorldSpawned { address: get_contract_address(), creator }); } #[abi(embed_v0)] impl World of IWorld { - /// Returns the metadata URI of the world. + /// Returns the metadata of the resource. /// - /// # Returns + /// # Arguments /// - /// * `Span` - The metadata URI of the world. - fn metadata_uri(self: @ContractState, resource: felt252) -> Span { - let mut uri = array![]; + /// `resource_id` - The resource id. + fn metadata(self: @ContractState, resource_id: felt252) -> ResourceMetadata { + let mut layout = array![]; + Introspect::::layout(ref layout); - // We add one here since we start i at 1; - let len = self.metadata_uri.read(resource) + 1; + let mut data = self + .entity(RESOURCE_METADATA_MODEL, array![resource_id].span(), layout.span(),); - let mut i = resource + 1; - loop { - if len == i { - break; - } + let mut model = array![resource_id]; + core::array::serialize_array_helper(data, ref model); - uri.append(self.metadata_uri.read(i)); - i += 1; - }; + let mut model_span = model.span(); - uri.span() + Serde::::deserialize(ref model_span).expect(Errors::METADATA_DESER) } - /// Sets the metadata URI of the world. + /// Sets the metadata of the resource. /// /// # Arguments /// - /// * `uri` - The new metadata URI to be set. - fn set_metadata_uri(ref self: ContractState, resource: felt252, mut uri: Span) { - assert(self.is_owner(get_caller_address(), resource), 'not owner'); - - let len = uri.len(); - - // Max len to avoid overflowing into other resources - assert(len < 255, 'metadata too long'); + /// `metadata` - The metadata content for the resource. + fn set_metadata(ref self: ContractState, metadata: ResourceMetadata) { + assert_can_write(@self, metadata.resource_id, get_caller_address()); - self.metadata_uri.write(resource, len.into()); + let model = Model::::name(@metadata); + let keys = Model::::keys(@metadata); + let values = Model::::values(@metadata); + let layout = Model::::layout(@metadata); - // Emit event before uri is consumed. - EventEmitter::emit(ref self, MetadataUpdate { resource, uri }); + let key = poseidon::poseidon_hash_span(keys); + database::set(model, key, values, layout); - let mut i = resource + 1; - loop { - match uri.pop_front() { - Option::Some(item) => { - self.metadata_uri.write(i, *item); - i += 1; - }, - Option::None(_) => { break; } - }; - }; + EventEmitter::emit(ref self, MetadataUpdate { resource: metadata.resource_id, uri: metadata.metadata_uri }); } /// Checks if the provided account is an owner of the resource. @@ -296,7 +260,7 @@ mod world { /// * `resource` - The resource. fn grant_owner(ref self: ContractState, address: ContractAddress, resource: felt252) { let caller = get_caller_address(); - assert(self.is_owner(caller, resource) || self.is_owner(caller, WORLD), 'not owner'); + assert(self.is_owner(caller, resource) || self.is_owner(caller, WORLD), Errors::NOT_OWNER); self.owners.write((resource, address), true); EventEmitter::emit(ref self, OwnerUpdated { address, resource, value: true }); @@ -311,8 +275,8 @@ mod world { /// * `resource` - The resource. fn revoke_owner(ref self: ContractState, address: ContractAddress, resource: felt252) { let caller = get_caller_address(); - assert(self.is_owner(caller, resource) || self.is_owner(caller, WORLD), 'not owner'); - self.owners.write((resource, address), bool::False(())); + assert(self.is_owner(caller, resource) || self.is_owner(caller, WORLD), Errors::NOT_OWNER); + self.owners.write((resource, address), false); EventEmitter::emit(ref self, OwnerUpdated { address, resource, value: false }); } @@ -342,7 +306,7 @@ mod world { let caller = get_caller_address(); assert( - self.is_owner(caller, model) || self.is_owner(caller, WORLD), 'not owner or writer' + self.is_owner(caller, model) || self.is_owner(caller, WORLD), Errors::NOT_OWNER_WRITER ); self.writers.write((model, system), true); @@ -363,7 +327,7 @@ mod world { self.is_writer(model, caller) || self.is_owner(caller, model) || self.is_owner(caller, WORLD), - 'not owner or writer' + Errors::NOT_OWNER_WRITER ); self.writers.write((model, system), false); @@ -378,21 +342,34 @@ mod world { /// * `class_hash` - The class hash of the model to be registered. fn register_model(ref self: ContractState, class_hash: ClassHash) { let caller = get_caller_address(); - let calldata = ArrayTrait::new(); - let name = *class_call(@self, class_hash, NAME_ENTRYPOINT, calldata.span())[0]; - let mut prev_class_hash = starknet::class_hash::ClassHashZeroable::zero(); + + let salt = self.models_count.read(); + let (address, name) = dojo::model::deploy_and_get_name(salt.into(), class_hash).unwrap_syscall(); + self.models_count.write(salt + 1); + + let (mut prev_class_hash, mut prev_address) = ( + starknet::class_hash::ClassHashZeroable::zero(), + starknet::contract_address::ContractAddressZeroable::zero(), + ); + + // Avoids a model name to conflict with already deployed contract, + // which can cause ACL issue with current ACL implementation. + if name.is_zero() || self.deployed_contracts.read(name).is_non_zero() { + panic_with_felt252(Errors::INVALID_MODEL_NAME); + } // If model is already registered, validate permission to update. - let current_class_hash = self.models.read(name); + let (current_class_hash, current_address) = self.models.read(name); if current_class_hash.is_non_zero() { - assert(self.is_owner(caller, name), 'only owner can update'); + assert(self.is_owner(caller, name), Errors::OWNER_ONLY_UPDATE); prev_class_hash = current_class_hash; + prev_address = current_address; } else { self.owners.write((name, caller), true); }; - self.models.write(name, class_hash); - EventEmitter::emit(ref self, ModelRegistered { name, class_hash, prev_class_hash }); + self.models.write(name, (class_hash, address)); + EventEmitter::emit(ref self, ModelRegistered { name, prev_address, address, class_hash, prev_class_hash }); } /// Gets the class hash of a registered model. @@ -403,8 +380,8 @@ mod world { /// /// # Returns /// - /// * `ClassHash` - The class hash of the model. - fn model(self: @ContractState, name: felt252) -> ClassHash { + /// * (`ClassHash`, `ContractAddress`) - The class hash and the contract address of the model. + fn model(self: @ContractState, name: felt252) -> (ClassHash, ContractAddress) { self.models.read(name) } @@ -412,8 +389,8 @@ mod world { /// /// # Arguments /// - /// * `name` - The name of the contract. - /// * `class_hash` - The class_hash of the contract. + /// * `salt` - The salt use for contract deployment. + /// * `class_hash` - The class hash of the contract. /// /// # Returns /// @@ -430,6 +407,8 @@ mod world { self.owners.write((contract_address.into(), get_caller_address()), true); + self.deployed_contracts.write(contract_address.into(), class_hash.into()); + EventEmitter::emit( ref self, ContractDeployed { salt, class_hash, address: contract_address } ); @@ -437,12 +416,12 @@ mod world { contract_address } - /// Upgrade an already deployed contract associated with the world. + /// Upgrades an already deployed contract associated with the world. /// /// # Arguments /// - /// * `name` - The name of the contract. - /// * `class_hash` - The class_hash of the contract. + /// * `address` - The contract address of the contract to upgrade. + /// * `class_hash` - The class hash of the contract. /// /// # Returns /// @@ -450,8 +429,7 @@ mod world { fn upgrade_contract( ref self: ContractState, address: ContractAddress, class_hash: ClassHash ) -> ClassHash { - // Only owner can upgrade contract - assert_can_write(@self, address.into(), get_caller_address()); + assert(is_account_owner(@self, address.into()), Errors::NOT_OWNER); IUpgradeableDispatcher { contract_address: address }.upgrade(class_hash); EventEmitter::emit(ref self, ContractUpgraded { class_hash, address }); class_hash @@ -485,31 +463,32 @@ mod world { /// # Arguments /// /// * `model` - The name of the model to be set. - /// * `key` - The key to be used to find the entity. - /// * `offset` - The offset of the model in the entity. - /// * `value` - The value to be set. + /// * `keys` - The key to be used to find the entity. + /// * `values` - The value to be set. + /// * `layout` - The memory layout of the entity. fn set_entity( ref self: ContractState, model: felt252, keys: Span, - offset: u8, values: Span, layout: Span ) { assert_can_write(@self, model, get_caller_address()); let key = poseidon::poseidon_hash_span(keys); - database::set(model, key, offset, values, layout); + database::set(model, key, values, layout); - EventEmitter::emit(ref self, StoreSetRecord { table: model, keys, offset, values }); + EventEmitter::emit(ref self, StoreSetRecord { table: model, keys, values }); } /// Deletes a model from an entity. + /// Deleting is setting all the values to 0 in the given layout. /// /// # Arguments /// /// * `model` - The name of the model to be deleted. - /// * `query` - The query to be used to find the entity. + /// * `keys` - The key to be used to find the entity. + /// * `layout` - The memory layout of the entity. fn delete_entity( ref self: ContractState, model: felt252, keys: Span, layout: Span ) { @@ -527,9 +506,7 @@ mod world { }; let key = poseidon::poseidon_hash_span(keys); - database::set(model, key, 0, empty_values.span(), layout); - // this deletes the index - database::del(model, key); + database::set(model, key, empty_values.span(), layout); EventEmitter::emit(ref self, StoreDelRecord { table: model, keys }); } @@ -540,89 +517,20 @@ mod world { /// # Arguments /// /// * `model` - The name of the model to be retrieved. - /// * `query` - The query to be used to find the entity. - /// * `offset` - The offset of the model values. - /// * `length` - The length of the model values. + /// * `keys` - The keys used to find the entity. + /// * `layout` - The memory layout of the entity. /// /// # Returns /// - /// * `Span` - The value of the model, zero initialized if not set. + /// * `Span` - The serialized value of the model, zero initialized if not set. fn entity( self: @ContractState, model: felt252, keys: Span, - offset: u8, - length: usize, layout: Span ) -> Span { let key = poseidon::poseidon_hash_span(keys); - database::get(model, key, offset, length, layout) - } - - /// Returns entity IDs and entities that contain the model state. - /// - /// # Arguments - /// - /// * `model` - The name of the model to be retrieved. - /// * `index` - The index to be retrieved. - /// * `values` - The query to be used to find the entity. - /// * `length` - The length of the model values. - /// - /// # Returns - /// - /// * `Span` - The entity IDs. - /// * `Span>` - The entities. - fn entities( - self: @ContractState, - model: felt252, - index: Option, - values: Span, - values_length: usize, - values_layout: Span - ) -> (Span, Span>) { - assert(values.len() == 0, 'Queries by values not impl'); - database::scan(model, Option::None(()), values_length, values_layout) - } - - /// Returns only the entity IDs that contain the model state. - /// # Arguments - /// * `model` - The name of the model to be retrieved. - /// * `index` - The index to be retrieved. - /// * `values` - The query to be used to find the entity. - /// * `length` - The length of the model values. - /// - /// # Returns - /// * `Span` - The entity IDs. - /// * `Span>` - The entities. - fn entity_ids(self: @ContractState, model: felt252) -> Span { - database::scan_ids(model, Option::None(())) - } - - /// Sets the executor contract address. - /// - /// # Arguments - /// - /// * `contract_address` - The contract address of the executor. - fn set_executor(ref self: ContractState, contract_address: ContractAddress) { - // Only owner can set executor - assert(self.is_owner(get_caller_address(), WORLD), 'only owner can set executor'); - let prev_address = self.executor_dispatcher.read().contract_address; - self - .executor_dispatcher - .write(IExecutorDispatcher { contract_address: contract_address }); - - EventEmitter::emit( - ref self, ExecutorUpdated { address: contract_address, prev_address } - ); - } - - /// Gets the executor contract address. - /// - /// # Returns - /// - /// * `ContractAddress` - The address of the executor contract. - fn executor(self: @ContractState) -> ContractAddress { - self.executor_dispatcher.read().contract_address + database::get(model, key, layout) } /// Gets the base contract class hash. @@ -638,7 +546,7 @@ mod world { #[abi(embed_v0)] impl UpgradeableWorld of IUpgradeableWorld { - /// Upgrade world with new_class_hash + /// Upgrades the world with new_class_hash /// /// # Arguments /// @@ -647,7 +555,7 @@ mod world { assert(new_class_hash.is_non_zero(), 'invalid class_hash'); assert( IWorld::is_owner(@self, get_tx_info().unbox().account_contract_address, WORLD), - 'only owner can upgrade' + Errors::OWNER_ONLY_UPGRADE, ); // upgrade to new_class_hash @@ -666,10 +574,24 @@ mod world { /// * `caller` - The name of the caller writing. fn assert_can_write(self: @ContractState, resource: felt252, caller: ContractAddress) { assert( - IWorld::is_writer(self, resource, caller) - || IWorld::is_owner(self, get_tx_info().unbox().account_contract_address, resource) - || IWorld::is_owner(self, get_tx_info().unbox().account_contract_address, WORLD), + IWorld::is_writer(self, resource, caller) || is_account_owner(self, resource), 'not writer' ); } + + /// Verifies if the calling account is owner of the resource or the + /// owner of the world. + /// + /// # Arguments + /// + /// * `resource` - The name of the resource being verified. + /// + /// # Returns + /// + /// * `bool` - True if the calling account is the owner of the resource or the owner of the world, + /// false otherwise. + fn is_account_owner(self: @ContractState, resource: felt252) -> bool { + IWorld::is_owner(self, get_tx_info().unbox().account_contract_address, resource) + || IWorld::is_owner(self, get_tx_info().unbox().account_contract_address, WORLD) + } } diff --git a/crates/dojo-core/src/world_test.cairo b/crates/dojo-core/src/world_test.cairo index 5122fd50d3..ae6ef28d56 100644 --- a/crates/dojo-core/src/world_test.cairo +++ b/crates/dojo-core/src/world_test.cairo @@ -8,8 +8,10 @@ use starknet::{contract_address_const, ContractAddress, ClassHash, get_caller_ad use starknet::syscalls::deploy_syscall; use dojo::benchmarks; -use dojo::executor::executor; -use dojo::world::{IWorldDispatcher, IWorldDispatcherTrait, world, IUpgradeableWorld, IUpgradeableWorldDispatcher, IUpgradeableWorldDispatcherTrait }; +use dojo::world::{ + IWorldDispatcher, IWorldDispatcherTrait, world, IUpgradeableWorld, IUpgradeableWorldDispatcher, + IUpgradeableWorldDispatcherTrait, ResourceMetadata +}; use dojo::database::introspect::Introspect; use dojo::test_utils::{spawn_test_world, deploy_with_world_address}; use dojo::benchmarks::{Character, end}; @@ -29,6 +31,24 @@ struct Fizz { a: felt252 } +#[starknet::interface] +trait INameOnly { + fn name(self: @T) -> felt252; +} + +#[starknet::contract] +mod resource_metadata_malicious { + #[storage] + struct Storage {} + + #[abi(embed_v0)] + impl InvalidModelName of super::INameOnly { + fn name(self: @ContractState) -> felt252 { + 'ResourceMetadata' + } + } +} + #[starknet::interface] trait Ibar { fn set_foo(self: @TContractState, a: felt252, b: u128); @@ -162,6 +182,7 @@ fn test_delete() { assert(deleted.a == 0, 'data not deleted'); assert(deleted.b == 0, 'data not deleted'); } +use core::debug::PrintTrait; #[test] #[available_gas(6000000)] @@ -169,8 +190,8 @@ fn test_model_class_hash_getter() { let world = deploy_world(); world.register_model(foo::TEST_CLASS_HASH.try_into().unwrap()); - let foo = world.model('Foo'); - assert(foo == foo::TEST_CLASS_HASH.try_into().unwrap(), 'foo does not exists'); + let (foo_class_hash, _) = world.model('Foo'); + assert(foo_class_hash == foo::TEST_CLASS_HASH.try_into().unwrap(), 'foo wrong class hash'); } #[test] @@ -246,65 +267,70 @@ fn deploy_world() -> IWorldDispatcher { #[test] #[available_gas(60000000)] -fn test_metadata_uri() { - // Deploy world contract +fn test_set_metadata_world() { let world = deploy_world(); - world.set_metadata_uri(0, array!['test_uri'].span()); - let uri = world.metadata_uri(0); - assert(uri.len() == 1, 'Incorrect metadata uri len'); - assert(uri[0] == @'test_uri', 'Incorrect metadata uri'); + let metadata = ResourceMetadata { resource_id: 0, metadata_uri: array_cap!(3, ('ipfs:world_with_a_long_uri_that', 'need_two_felts/1.json')).span() }; - world.set_metadata_uri(0, array!['new_uri', 'longer'].span()); + world.set_metadata(metadata.clone()); - let uri = world.metadata_uri(0); - assert(uri.len() == 2, 'Incorrect metadata uri len'); - assert(uri[0] == @'new_uri', 'Incorrect metadata uri 1'); - assert(uri[1] == @'longer', 'Incorrect metadata uri 2'); + assert(world.metadata(0) == metadata, 'invalid metadata'); } #[test] #[available_gas(60000000)] -#[should_panic] -fn test_set_metadata_uri_reverts_for_not_owner() { - // Deploy world contract - let world = deploy_world(); - - starknet::testing::set_contract_address(starknet::contract_address_const::<0x1337>()); - world.set_metadata_uri(0, array!['new_uri', 'longer'].span()); -} - -#[test] -#[available_gas(60000000)] -fn test_entities() { - // Deploy world contract +fn test_set_metadata_model_writer() { let world = spawn_test_world(array![foo::TEST_CLASS_HASH],); let bar_contract = IbarDispatcher { contract_address: deploy_with_world_address(bar::TEST_CLASS_HASH, world) }; - let alice = starknet::contract_address_const::<0x1337>(); - starknet::testing::set_contract_address(alice); + world.grant_writer('Foo', bar_contract.contract_address); + + let bob = starknet::contract_address_const::<0xb0b>(); + starknet::testing::set_account_contract_address(bob); + starknet::testing::set_contract_address(bar_contract.contract_address); + bar_contract.set_foo(1337, 1337); - let mut keys = ArrayTrait::new(); - keys.append(0); + let metadata = ResourceMetadata { resource_id: 'Foo', metadata_uri: array_cap!(3, ('ipfs:bob',)).span(), }; + + // A system that has write access on a model should be able to update the metadata. + // This follows conventional ACL model. + world.set_metadata(metadata.clone()); + assert(world.metadata('Foo') == metadata, 'bad metadata'); +} + +#[test] +#[available_gas(60000000)] +#[should_panic(expected: ('not writer', 'ENTRYPOINT_FAILED',))] +fn test_set_metadata_same_model_rules() { + let world = deploy_world(); + + let metadata = ResourceMetadata { // World metadata. + resource_id: 0, metadata_uri: array_cap!(10, ('ipfs:bob',)).span(), }; - let mut query_keys = ArrayTrait::new(); - let layout = array![251].span(); - let (keys, _values) = world.entities('Foo', Option::None, query_keys.span(), 2, layout); - let ids = world.entity_ids('Foo'); - assert(keys.len() == ids.len(), 'result differs in entity_ids'); - assert(keys.len() == 0, 'found value for unindexed'); -// query_keys.append(0x1337); -// let (keys, values) = world.entities('Foo', 42, query_keys.span(), 2, layout); -// assert(keys.len() == 1, 'No keys found!'); + let bob = starknet::contract_address_const::<0xb0b>(); + starknet::testing::set_contract_address(bob); + starknet::testing::set_account_contract_address(bob); -// let mut query_keys = ArrayTrait::new(); -// query_keys.append(0x1338); -// let (keys, values) = world.entities('Foo', 42, query_keys.span(), 2, layout); -// assert(keys.len() == 0, 'Keys found!'); + // Bob access follows the conventional ACL, he can't write the world + // metadata if he does not have access to it. + world.set_metadata(metadata); +} + +#[test] +#[available_gas(60000000)] +#[should_panic(expected: ('only owner can update', 'ENTRYPOINT_FAILED',))] +fn test_metadata_update_owner_only() { + let world = deploy_world(); + + let bob = starknet::contract_address_const::<0xb0b>(); + starknet::testing::set_contract_address(bob); + starknet::testing::set_account_contract_address(bob); + + world.register_model(resource_metadata_malicious::TEST_CLASS_HASH.try_into().unwrap()); } #[test] @@ -505,7 +531,7 @@ mod worldupgrade { #[abi(embed_v0)] impl IWorldUpgradeImpl of super::IWorldUpgrade { - fn hello(self: @ContractState) -> felt252{ + fn hello(self: @ContractState) -> felt252 { 'dojo' } } @@ -515,7 +541,6 @@ mod worldupgrade { #[test] #[available_gas(60000000)] fn test_upgradeable_world() { - // Deploy world contract let world = deploy_world(); @@ -524,18 +549,15 @@ fn test_upgradeable_world() { }; upgradeable_world_dispatcher.upgrade(worldupgrade::TEST_CLASS_HASH.try_into().unwrap()); - let res = (IWorldUpgradeDispatcher { - contract_address: world.contract_address - }).hello(); + let res = (IWorldUpgradeDispatcher { contract_address: world.contract_address }).hello(); assert(res == 'dojo', 'should return dojo'); } #[test] #[available_gas(60000000)] -#[should_panic(expected:('invalid class_hash', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('invalid class_hash', 'ENTRYPOINT_FAILED'))] fn test_upgradeable_world_with_class_hash_zero() { - // Deploy world contract let world = deploy_world(); @@ -549,9 +571,8 @@ fn test_upgradeable_world_with_class_hash_zero() { #[test] #[available_gas(60000000)] -#[should_panic( expected: ('only owner can upgrade', 'ENTRYPOINT_FAILED'))] +#[should_panic(expected: ('only owner can upgrade', 'ENTRYPOINT_FAILED'))] fn test_upgradeable_world_from_non_owner() { - // Deploy world contract let world = deploy_world(); diff --git a/crates/dojo-lang/Cargo.toml b/crates/dojo-lang/Cargo.toml index 1d1da8d84c..60ee9f7bdd 100644 --- a/crates/dojo-lang/Cargo.toml +++ b/crates/dojo-lang/Cargo.toml @@ -34,6 +34,7 @@ dojo-world = { path = "../dojo-world", features = [ "manifest" ] } indoc.workspace = true itertools.workspace = true lazy_static.workspace = true +num-traits.workspace = true once_cell.workspace = true salsa.workspace = true scarb.workspace = true diff --git a/crates/dojo-lang/src/compiler.rs b/crates/dojo-lang/src/compiler.rs index 386351083f..6591d6541c 100644 --- a/crates/dojo-lang/src/compiler.rs +++ b/crates/dojo-lang/src/compiler.rs @@ -16,7 +16,7 @@ use cairo_lang_starknet::plugin::aux_data::StarkNetContractAuxData; use cairo_lang_utils::UpcastMut; use convert_case::{Case, Casing}; use dojo_world::manifest::{ - Class, ComputedValueEntrypoint, Contract, BASE_CONTRACT_NAME, EXECUTOR_CONTRACT_NAME, + Class, ComputedValueEntrypoint, Contract, BASE_CONTRACT_NAME, RESOURCE_METADATA_CONTRACT_NAME, WORLD_CONTRACT_NAME, }; use itertools::Itertools; @@ -187,7 +187,6 @@ fn find_project_contracts( pub fn collect_core_crate_ids(db: &RootDatabase) -> Vec { [ ContractSelector(BASE_CONTRACT_NAME.to_string()), - ContractSelector(EXECUTOR_CONTRACT_NAME.to_string()), ContractSelector(WORLD_CONTRACT_NAME.to_string()), ] .iter() @@ -237,22 +236,22 @@ fn update_manifest( } }; - let executor = { + let base = { + let (hash, abi) = get_compiled_artifact_from_map(&compiled_artifacts, BASE_CONTRACT_NAME)?; + Class { name: BASE_CONTRACT_NAME.into(), abi: abi.clone(), class_hash: *hash } + }; + + let resource_metadata = { let (hash, abi) = - get_compiled_artifact_from_map(&compiled_artifacts, EXECUTOR_CONTRACT_NAME)?; + get_compiled_artifact_from_map(&compiled_artifacts, RESOURCE_METADATA_CONTRACT_NAME)?; Contract { - name: EXECUTOR_CONTRACT_NAME.into(), + name: RESOURCE_METADATA_CONTRACT_NAME.into(), abi: abi.clone(), class_hash: *hash, ..Default::default() } }; - let base = { - let (hash, abi) = get_compiled_artifact_from_map(&compiled_artifacts, BASE_CONTRACT_NAME)?; - Class { name: BASE_CONTRACT_NAME.into(), abi: abi.clone(), class_hash: *hash } - }; - let mut models = BTreeMap::new(); let mut contracts = BTreeMap::new(); let mut computed = BTreeMap::new(); @@ -305,7 +304,7 @@ fn update_manifest( contracts.remove(model.0.as_str()); } - do_update_manifest(manifest, world, executor, base, models, contracts)?; + do_update_manifest(manifest, world, base, resource_metadata, models, contracts)?; Ok(()) } @@ -383,7 +382,7 @@ fn get_dojo_contract_artifacts( let mut result = HashMap::new(); - if !matches!(contract_name.as_ref(), "world" | "executor" | "base") { + if !matches!(contract_name.as_ref(), "world" | "resource_metadata" | "base") { let module_name: SmolStr = module_id.full_path(db).into(); if let Some((class_hash, abi)) = compiled_classes.get(&module_name as &str) { @@ -420,8 +419,8 @@ fn get_dojo_contract_artifacts( fn do_update_manifest( current_manifest: &mut dojo_world::manifest::Manifest, world: dojo_world::manifest::Contract, - executor: dojo_world::manifest::Contract, base: dojo_world::manifest::Class, + resource_metadata: dojo_world::manifest::Contract, models: BTreeMap, contracts: BTreeMap, ) -> anyhow::Result<()> { @@ -429,14 +428,14 @@ fn do_update_manifest( current_manifest.world = world; } - if current_manifest.executor.class_hash != executor.class_hash { - current_manifest.executor = executor; - } - if current_manifest.base.class_hash != base.class_hash { current_manifest.base = base; } + if current_manifest.resource_metadata.class_hash != resource_metadata.class_hash { + current_manifest.resource_metadata = resource_metadata; + } + let mut contracts_to_add = vec![]; for (name, mut contract) in contracts { if let Some(existing_contract) = diff --git a/crates/dojo-lang/src/compiler_test.rs b/crates/dojo-lang/src/compiler_test.rs index 97772627c2..d295bfd3fb 100644 --- a/crates/dojo-lang/src/compiler_test.rs +++ b/crates/dojo-lang/src/compiler_test.rs @@ -5,7 +5,9 @@ use std::{env, fs}; use cairo_lang_test_utils::parse_test_file::TestRunnerResult; use cairo_lang_utils::ordered_hash_map::OrderedHashMap; use dojo_test_utils::compiler::build_test_config; -use dojo_world::manifest::{BASE_CONTRACT_NAME, EXECUTOR_CONTRACT_NAME, WORLD_CONTRACT_NAME}; +use dojo_world::manifest::{ + BASE_CONTRACT_NAME, RESOURCE_METADATA_CONTRACT_NAME, WORLD_CONTRACT_NAME, +}; use scarb::core::TargetKind; use scarb::ops::CompileOpts; use smol_str::SmolStr; @@ -23,18 +25,17 @@ fn build_mock_manifest() -> dojo_world::manifest::Manifest { class_hash: felt!("0xdeadbeef"), ..Default::default() }, - executor: dojo_world::manifest::Contract { - name: EXECUTOR_CONTRACT_NAME.into(), - abi: None, - address: Some(felt!("0x1234")), - class_hash: felt!("0x4567"), - ..Default::default() - }, base: dojo_world::manifest::Class { name: BASE_CONTRACT_NAME.into(), class_hash: felt!("0x9090"), ..Default::default() }, + resource_metadata: dojo_world::manifest::Contract { + name: RESOURCE_METADATA_CONTRACT_NAME.into(), + class_hash: felt!("0x1111"), + address: Some(felt!("0x1234")), + ..Default::default() + }, contracts: vec![ dojo_world::manifest::Contract { name: "TestContract1".into(), @@ -76,8 +77,8 @@ fn update_manifest_correctly() { let mut mock_manifest = build_mock_manifest(); let world = mock_manifest.world.clone(); - let executor = mock_manifest.executor.clone(); let base = mock_manifest.base.clone(); + let resource_metadata = mock_manifest.resource_metadata.clone(); let contracts = mock_manifest.contracts.clone(); let new_models: BTreeMap = [( @@ -124,15 +125,14 @@ fn update_manifest_correctly() { do_update_manifest( &mut mock_manifest, world.clone(), - executor.clone(), base.clone(), + resource_metadata.clone(), new_models.clone(), new_contracts, ) .unwrap(); assert!(mock_manifest.world == world, "world should not change"); - assert!(mock_manifest.executor == executor, "executor should not change"); assert!(mock_manifest.base == base, "base should not change"); assert!(mock_manifest.models == new_models.into_values().collect::>()); diff --git a/crates/dojo-lang/src/inline_macros/array_cap.rs b/crates/dojo-lang/src/inline_macros/array_cap.rs new file mode 100644 index 0000000000..340ced044c --- /dev/null +++ b/crates/dojo-lang/src/inline_macros/array_cap.rs @@ -0,0 +1,131 @@ +use cairo_lang_defs::patcher::PatchBuilder; +use cairo_lang_defs::plugin::{ + InlineMacroExprPlugin, InlinePluginResult, NamedPlugin, PluginDiagnostic, PluginGeneratedFile, +}; +use cairo_lang_diagnostics::Severity; +use cairo_lang_semantic::inline_macros::unsupported_bracket_diagnostic; +use cairo_lang_syntax::node::{ast, TypedSyntaxNode}; + +use super::unsupported_arg_diagnostic; + +#[derive(Debug, Default)] +pub struct ArrayCapMacro; + +impl NamedPlugin for ArrayCapMacro { + const NAME: &'static str = "array_cap"; +} + +impl InlineMacroExprPlugin for ArrayCapMacro { + fn generate_code( + &self, + db: &dyn cairo_lang_syntax::node::db::SyntaxGroup, + syntax: &ast::ExprInlineMacro, + ) -> InlinePluginResult { + let ast::WrappedArgList::ParenthesizedArgList(arg_list) = syntax.arguments(db) else { + return unsupported_bracket_diagnostic(db, syntax); + }; + + let mut builder = PatchBuilder::new(db); + + builder.add_str("{"); + builder.add_str("let mut __array_with_cap__ = array![];"); + + let args = arg_list.arguments(db).elements(db); + + if args.is_empty() || args.len() > 2 { + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: arg_list.arguments(db).stable_ptr().untyped(), + message: "Invalid arguments. Expected \"(capacity, (values,))\"".to_string(), + severity: Severity::Error, + }], + }; + } + + let capacity = match (args[0]).as_syntax_node().get_text(db).parse::() { + Ok(c) => c, + Err(_) => { + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: arg_list.arguments(db).stable_ptr().untyped(), + message: "Invalid capacity, usize expected".to_string(), + severity: Severity::Error, + }], + }; + } + }; + + let bundle = if args.len() == 1 { + let values: Vec = vec!["0"; capacity].iter().map(|&s| s.to_string()).collect(); + values + } else { + // 2 args, we parse the user values and fill with 0 if necessary. + let ast::ArgClause::Unnamed(values) = args[1].arg_clause(db) else { + return unsupported_arg_diagnostic(db, syntax); + }; + + let mut bundle = vec![]; + + match values.value(db) { + ast::Expr::Tuple(list) => { + let mut i = 0; + for expr in list.expressions(db).elements(db).into_iter() { + if i > capacity { + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + stable_ptr: expr.stable_ptr().untyped(), + message: "Number of values is exceeded the capacity" + .to_string(), + severity: Severity::Error, + }], + }; + } + + let syntax_node = expr.as_syntax_node(); + bundle.push(syntax_node.get_text(db)); + i += 1; + } + + if i < capacity { + for _ in i..capacity { + bundle.push("0".to_string()); + } + } + } + _ => { + return InlinePluginResult { + code: None, + diagnostics: vec![PluginDiagnostic { + message: "Invalid arguments. Expected \"(capacity, (values,))\"" + .to_string(), + stable_ptr: arg_list.arguments(db).stable_ptr().untyped(), + severity: Severity::Error, + }], + }; + } + }; + + bundle + }; + + for value in bundle { + builder.add_str(&format!("__array_with_cap__.append({});", value)); + } + + builder.add_str("__array_with_cap__"); + builder.add_str("}"); + + InlinePluginResult { + code: Some(PluginGeneratedFile { + name: "array_cap_inline_macro".into(), + content: builder.code, + code_mappings: builder.code_mappings, + aux_data: None, + }), + diagnostics: vec![], + } + } +} diff --git a/crates/dojo-lang/src/inline_macros/get.rs b/crates/dojo-lang/src/inline_macros/get.rs index 96bbf3ca61..3475a1ecff 100644 --- a/crates/dojo-lang/src/inline_macros/get.rs +++ b/crates/dojo-lang/src/inline_macros/get.rs @@ -123,8 +123,7 @@ impl InlineMacroExprPlugin for GetMacro { core::array::ArrayTrait::span(@__{model}_layout__); let mut __{model}_layout_clone_span__ = \ core::array::ArrayTrait::span(@__{model}_layout_clone__); - let mut __{model}_values__ = {}.entity('{model}', __get_macro_keys__, 0_u8, - dojo::packing::calculate_packed_size(ref __{model}_layout_clone_span__), + let mut __{model}_values__ = {}.entity('{model}', __get_macro_keys__, __{model}_layout_span__); let mut __{model}_model__ = core::array::ArrayTrait::new(); core::array::serialize_array_helper(__get_macro_keys__, ref __{model}_model__); diff --git a/crates/dojo-lang/src/inline_macros/mod.rs b/crates/dojo-lang/src/inline_macros/mod.rs index ac1da2b595..c1c701183e 100644 --- a/crates/dojo-lang/src/inline_macros/mod.rs +++ b/crates/dojo-lang/src/inline_macros/mod.rs @@ -4,6 +4,7 @@ use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::{ast, Terminal, TypedSyntaxNode}; use smol_str::SmolStr; +pub mod array_cap; pub mod delete; pub mod emit; pub mod get; diff --git a/crates/dojo-lang/src/inline_macros/set.rs b/crates/dojo-lang/src/inline_macros/set.rs index fe4a842bf1..39e4e022e7 100644 --- a/crates/dojo-lang/src/inline_macros/set.rs +++ b/crates/dojo-lang/src/inline_macros/set.rs @@ -161,7 +161,7 @@ impl InlineMacroExprPlugin for SetMacro { " let __set_macro_value__ = {}; {}.set_entity(dojo::model::Model::name(@__set_macro_value__), - dojo::model::Model::keys(@__set_macro_value__), 0_u8, + dojo::model::Model::keys(@__set_macro_value__), dojo::model::Model::values(@__set_macro_value__), dojo::model::Model::layout(@__set_macro_value__));", entity, diff --git a/crates/dojo-lang/src/introspect.rs b/crates/dojo-lang/src/introspect.rs index 5c9978b63b..0c9feb61d0 100644 --- a/crates/dojo-lang/src/introspect.rs +++ b/crates/dojo-lang/src/introspect.rs @@ -3,15 +3,22 @@ use std::collections::HashMap; use cairo_lang_defs::patcher::RewriteNode; use cairo_lang_defs::plugin::PluginDiagnostic; use cairo_lang_diagnostics::Severity; +use cairo_lang_syntax::attribute::structured::{ + Attribute, AttributeArg, AttributeArgVariant, AttributeListStructurize, +}; use cairo_lang_syntax::node::ast::{ - Expr, GenericParam, ItemEnum, ItemStruct, OptionTypeClause, OptionWrappedGenericParamList, + self, Expr, GenericParam, ItemEnum, ItemStruct, OptionTypeClause, OptionWrappedGenericParamList, }; use cairo_lang_syntax::node::db::SyntaxGroup; use cairo_lang_syntax::node::helpers::QueryAttrs; use cairo_lang_syntax::node::{Terminal, TypedSyntaxNode}; use cairo_lang_utils::unordered_hash_map::UnorderedHashMap; +use cairo_lang_utils::OptionHelper; use dojo_world::manifest::Member; use itertools::Itertools; +use num_traits::ToPrimitive; + +const ARRAY_CAPACITY_ATTR: &str = "capacity"; #[derive(Clone, Default)] struct TypeIntrospection(usize, Vec); @@ -38,7 +45,11 @@ fn primitive_type_introspection() -> HashMap { /// * struct_ast: The AST of the struct. /// Returns: /// * A RewriteNode containing the generated code. -pub fn handle_introspect_struct(db: &dyn SyntaxGroup, struct_ast: ItemStruct) -> RewriteNode { +pub fn handle_introspect_struct( + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, + struct_ast: ItemStruct, +) -> RewriteNode { let name = struct_ast.name(db).text(db).into(); let mut member_types: Vec = vec![]; @@ -50,7 +61,14 @@ pub fn handle_introspect_struct(db: &dyn SyntaxGroup, struct_ast: ItemStruct) -> .iter() .map(|member| { let key = member.has_attr(db, "key"); - let ty = member.type_clause(db).ty(db).as_syntax_node().get_text(db).trim().to_string(); + + let attrs = member.attributes(db).structurize(db); + let array_capacity_attr = + attrs.iter().find(|attr| attr.id.as_str() == ARRAY_CAPACITY_ATTR); + let capacity = extract_array_capacity(array_capacity_attr, db, diagnostics); + + let mut ty = + member.type_clause(db).ty(db).as_syntax_node().get_text(db).trim().to_string(); let name = member.name(db).text(db).to_string(); let mut attrs = vec![]; if key { @@ -68,6 +86,35 @@ pub fn handle_introspect_struct(db: &dyn SyntaxGroup, struct_ast: ItemStruct) -> }})", attrs.join(","), )); + } else if let Some(c) = capacity { + if c == 0 { + diagnostics.push(PluginDiagnostic { + stable_ptr: member.stable_ptr().0, + message: "Capacity must be greater than 0.".to_string(), + severity: Severity::Error, + }); + } + + if &ty != "Array" && &ty != "Span" { + diagnostics.push(PluginDiagnostic { + stable_ptr: member.stable_ptr().0, + message: "Capacity is only supported for Array or Span." + .to_string(), + severity: Severity::Error, + }); + } + + member_types.push(format!( + "dojo::database::introspect::serialize_member(@\ + dojo::database::introspect::Member {{ + name: '{name}', + ty: dojo::database::introspect::Ty::Array({c}), + attrs: array![{}].span() + }})", + attrs.join(","), + )); + + ty = format!("array_felts__{c}"); } else { // It's a custom struct/enum member_types.push(format!( @@ -264,6 +311,23 @@ fn handle_introspect_internal( layout.push(RewriteNode::Text(format!("layout.append({});\n", l))) }); } + } else if m.ty.starts_with("array_felts__") { + let capacity = + m.ty.strip_prefix("array_felts__") + .unwrap() + .parse::() + .expect("u32 expected for array capacity"); + + if m.key { + attrs.push("'key'"); + } else { + // Serialized array always have their length first. + size.push(format!("1 + {capacity}")); + + for _i in 0..=capacity { + layout.push(RewriteNode::Text("layout.append(251);\n".to_string())) + } + } } else { // It's a custom type if m.key { @@ -310,3 +374,34 @@ impl $name$Introspect<$generics$> of \ ]), ) } + +/// Extract the array capacity from the attribute. +/// Adds a diagnostic if the attribute is malformed. +fn extract_array_capacity( + capacity_attr: Option<&Attribute>, + db: &dyn SyntaxGroup, + diagnostics: &mut Vec, +) -> Option { + let Some(attr) = capacity_attr else { + return None; + }; + + #[allow(clippy::collapsible_match)] + match &attr.args[..] { + [AttributeArg { variant: AttributeArgVariant::Unnamed { value, .. }, .. }] => match value { + ast::Expr::Literal(literal) => { + literal.numeric_value(db).and_then(|v| v.to_u32()).and_then(|v| v.to_usize()) + } + _ => None, + }, + _ => None, + } + .on_none(|| { + diagnostics.push(PluginDiagnostic { + stable_ptr: attr.args_stable_ptr.untyped(), + message: "Attribute should have a single non-negative literal in `u32` range." + .to_string(), + severity: Severity::Error, + }) + }) +} diff --git a/crates/dojo-lang/src/manifest_test_data/cairo_v240 b/crates/dojo-lang/src/manifest_test_data/cairo_v240 index 97cc9cff09..0a9e895b3f 100644 --- a/crates/dojo-lang/src/manifest_test_data/cairo_v240 +++ b/crates/dojo-lang/src/manifest_test_data/cairo_v240 @@ -8,7 +8,7 @@ test_compiler_cairo_v240 "world": { "name": "dojo::world::world", "address": null, - "class_hash": "0x54afaf1c0d052df74af180637871455cafeee493e2f98a1e09fadcd8b36ef14", + "class_hash": "0x6c0c550736dd694219f1a3d0a8ac95fbc8b17382f1f02dc09422451bfa30376", "abi": [ { "type": "impl", @@ -27,35 +27,25 @@ test_compiler_cairo_v240 }, { "type": "struct", - "name": "core::array::Span::", + "name": "dojo::resource_metadata::ResourceMetadata", "members": [ { - "name": "snapshot", - "type": "@core::array::Array::" - } - ] - }, - { - "type": "enum", - "name": "core::option::Option::", - "variants": [ - { - "name": "Some", + "name": "resource_id", "type": "core::felt252" }, { - "name": "None", - "type": "()" + "name": "metadata_uri", + "type": "core::array::Span::" } ] }, { "type": "struct", - "name": "core::array::Span::>", + "name": "core::array::Span::", "members": [ { "name": "snapshot", - "type": "@core::array::Array::>" + "type": "@core::array::Array::" } ] }, @@ -79,31 +69,27 @@ test_compiler_cairo_v240 "items": [ { "type": "function", - "name": "metadata_uri", + "name": "metadata", "inputs": [ { - "name": "resource", + "name": "resource_id", "type": "core::felt252" } ], "outputs": [ { - "type": "core::array::Span::" + "type": "dojo::resource_metadata::ResourceMetadata" } ], "state_mutability": "view" }, { "type": "function", - "name": "set_metadata_uri", + "name": "set_metadata", "inputs": [ { - "name": "resource", - "type": "core::felt252" - }, - { - "name": "uri", - "type": "core::array::Span::" + "name": "metadata", + "type": "dojo::resource_metadata::ResourceMetadata" } ], "outputs": [], @@ -120,7 +106,7 @@ test_compiler_cairo_v240 ], "outputs": [ { - "type": "core::starknet::class_hash::ClassHash" + "type": "(core::starknet::class_hash::ClassHash, core::starknet::contract_address::ContractAddress)" } ], "state_mutability": "view" @@ -216,14 +202,6 @@ test_compiler_cairo_v240 "name": "keys", "type": "core::array::Span::" }, - { - "name": "offset", - "type": "core::integer::u8" - }, - { - "name": "length", - "type": "core::integer::u32" - }, { "name": "layout", "type": "core::array::Span::" @@ -248,10 +226,6 @@ test_compiler_cairo_v240 "name": "keys", "type": "core::array::Span::" }, - { - "name": "offset", - "type": "core::integer::u8" - }, { "name": "values", "type": "core::array::Span::" @@ -264,77 +238,6 @@ test_compiler_cairo_v240 "outputs": [], "state_mutability": "external" }, - { - "type": "function", - "name": "entities", - "inputs": [ - { - "name": "model", - "type": "core::felt252" - }, - { - "name": "index", - "type": "core::option::Option::" - }, - { - "name": "values", - "type": "core::array::Span::" - }, - { - "name": "values_length", - "type": "core::integer::u32" - }, - { - "name": "values_layout", - "type": "core::array::Span::" - } - ], - "outputs": [ - { - "type": "(core::array::Span::, core::array::Span::>)" - } - ], - "state_mutability": "view" - }, - { - "type": "function", - "name": "entity_ids", - "inputs": [ - { - "name": "model", - "type": "core::felt252" - } - ], - "outputs": [ - { - "type": "core::array::Span::" - } - ], - "state_mutability": "view" - }, - { - "type": "function", - "name": "set_executor", - "inputs": [ - { - "name": "contract_address", - "type": "core::starknet::contract_address::ContractAddress" - } - ], - "outputs": [], - "state_mutability": "external" - }, - { - "type": "function", - "name": "executor", - "inputs": [], - "outputs": [ - { - "type": "core::starknet::contract_address::ContractAddress" - } - ], - "state_mutability": "view" - }, { "type": "function", "name": "base", @@ -499,10 +402,6 @@ test_compiler_cairo_v240 "type": "constructor", "name": "constructor", "inputs": [ - { - "name": "executor", - "type": "core::starknet::contract_address::ContractAddress" - }, { "name": "contract_base", "type": "core::starknet::class_hash::ClassHash" @@ -613,6 +512,16 @@ test_compiler_cairo_v240 "name": "prev_class_hash", "type": "core::starknet::class_hash::ClassHash", "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "prev_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" } ] }, @@ -631,11 +540,6 @@ test_compiler_cairo_v240 "type": "core::array::Span::", "kind": "data" }, - { - "name": "offset", - "type": "core::integer::u8", - "kind": "data" - }, { "name": "values", "type": "core::array::Span::", @@ -704,23 +608,6 @@ test_compiler_cairo_v240 } ] }, - { - "type": "event", - "name": "dojo::world::world::ExecutorUpdated", - "kind": "struct", - "members": [ - { - "name": "address", - "type": "core::starknet::contract_address::ContractAddress", - "kind": "data" - }, - { - "name": "prev_address", - "type": "core::starknet::contract_address::ContractAddress", - "kind": "data" - } - ] - }, { "type": "event", "name": "dojo::world::world::Event", @@ -775,11 +662,6 @@ test_compiler_cairo_v240 "name": "OwnerUpdated", "type": "dojo::world::world::OwnerUpdated", "kind": "nested" - }, - { - "name": "ExecutorUpdated", - "type": "dojo::world::world::ExecutorUpdated", - "kind": "nested" } ] } @@ -788,70 +670,9 @@ test_compiler_cairo_v240 "writes": [], "computed": [] }, - "executor": { - "name": "dojo::executor::executor", - "address": null, - "class_hash": "0x55865ef05f918b04b08aa1588e9a49e80cbaecdf13e1438c142957e9aedaf73", - "abi": [ - { - "type": "impl", - "name": "Executor", - "interface_name": "dojo::executor::IExecutor" - }, - { - "type": "struct", - "name": "core::array::Span::", - "members": [ - { - "name": "snapshot", - "type": "@core::array::Array::" - } - ] - }, - { - "type": "interface", - "name": "dojo::executor::IExecutor", - "items": [ - { - "type": "function", - "name": "call", - "inputs": [ - { - "name": "class_hash", - "type": "core::starknet::class_hash::ClassHash" - }, - { - "name": "entrypoint", - "type": "core::felt252" - }, - { - "name": "calldata", - "type": "core::array::Span::" - } - ], - "outputs": [ - { - "type": "core::array::Span::" - } - ], - "state_mutability": "view" - } - ] - }, - { - "type": "event", - "name": "dojo::executor::executor::Event", - "kind": "enum", - "variants": [] - } - ], - "reads": [], - "writes": [], - "computed": [] - }, "base": { "name": "dojo::base::base", - "class_hash": "0x48bca802a22c7d7e447cf62c62581acf5f1eae00554d49e2279b0b0132a1777", + "class_hash": "0x794d5ed2f7eb970f92e0ed9be8f73bbbdf18f7db2a9a296fa12c2d9c33e6ab3", "abi": [ { "type": "impl", @@ -951,6 +772,205 @@ test_compiler_cairo_v240 } ] }, + "resource_metadata": { + "name": "dojo::resource_metadata::resource_metadata", + "address": null, + "class_hash": "0x6a2f06cde4aad60e0b6dd595edebe8dca1fbefe5b36cfc2f46a1d1159757df9", + "abi": [ + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "unpacked_size", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "packed_size", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "function", + "name": "layout", + "inputs": [], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "view" + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::>", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::>" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Struct", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "children", + "type": "core::array::Span::>" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::<(core::felt252, core::array::Span::)>", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::<(core::felt252, core::array::Span::)>" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Enum", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "children", + "type": "core::array::Span::<(core::felt252, core::array::Span::)>" + } + ] + }, + { + "type": "enum", + "name": "dojo::database::introspect::Ty", + "variants": [ + { + "name": "Primitive", + "type": "core::felt252" + }, + { + "name": "Struct", + "type": "dojo::database::introspect::Struct" + }, + { + "name": "Enum", + "type": "dojo::database::introspect::Enum" + }, + { + "name": "Tuple", + "type": "core::array::Span::>" + }, + { + "name": "Array", + "type": "core::integer::u32" + } + ] + }, + { + "type": "function", + "name": "schema", + "inputs": [], + "outputs": [ + { + "type": "dojo::database::introspect::Ty" + } + ], + "state_mutability": "view" + }, + { + "type": "struct", + "name": "dojo::resource_metadata::ResourceMetadata", + "members": [ + { + "name": "resource_id", + "type": "core::felt252" + }, + { + "name": "metadata_uri", + "type": "core::array::Span::" + } + ] + }, + { + "type": "function", + "name": "ensure_abi", + "inputs": [ + { + "name": "model", + "type": "dojo::resource_metadata::ResourceMetadata" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "event", + "name": "dojo::resource_metadata::resource_metadata::Event", + "kind": "enum", + "variants": [] + } + ], + "reads": [], + "writes": [], + "computed": [] + }, "contracts": [ { "name": "cairo_v240::cairo_v240", diff --git a/crates/dojo-lang/src/manifest_test_data/compiler_cairo_v240/Scarb.lock b/crates/dojo-lang/src/manifest_test_data/compiler_cairo_v240/Scarb.lock index bb593d0412..b2780e275d 100644 --- a/crates/dojo-lang/src/manifest_test_data/compiler_cairo_v240/Scarb.lock +++ b/crates/dojo-lang/src/manifest_test_data/compiler_cairo_v240/Scarb.lock @@ -10,7 +10,7 @@ dependencies = [ [[package]] name = "dojo" -version = "0.5.0" +version = "0.5.1" dependencies = [ "dojo_plugin", ] diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index c2795c0c34..5d15e0659c 100644 --- a/crates/dojo-lang/src/manifest_test_data/manifest +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -8,7 +8,7 @@ test_manifest_file "world": { "name": "dojo::world::world", "address": null, - "class_hash": "0x54afaf1c0d052df74af180637871455cafeee493e2f98a1e09fadcd8b36ef14", + "class_hash": "0x6c0c550736dd694219f1a3d0a8ac95fbc8b17382f1f02dc09422451bfa30376", "abi": [ { "type": "impl", @@ -27,35 +27,25 @@ test_manifest_file }, { "type": "struct", - "name": "core::array::Span::", + "name": "dojo::resource_metadata::ResourceMetadata", "members": [ { - "name": "snapshot", - "type": "@core::array::Array::" - } - ] - }, - { - "type": "enum", - "name": "core::option::Option::", - "variants": [ - { - "name": "Some", + "name": "resource_id", "type": "core::felt252" }, { - "name": "None", - "type": "()" + "name": "metadata_uri", + "type": "core::array::Span::" } ] }, { "type": "struct", - "name": "core::array::Span::>", + "name": "core::array::Span::", "members": [ { "name": "snapshot", - "type": "@core::array::Array::>" + "type": "@core::array::Array::" } ] }, @@ -79,31 +69,27 @@ test_manifest_file "items": [ { "type": "function", - "name": "metadata_uri", + "name": "metadata", "inputs": [ { - "name": "resource", + "name": "resource_id", "type": "core::felt252" } ], "outputs": [ { - "type": "core::array::Span::" + "type": "dojo::resource_metadata::ResourceMetadata" } ], "state_mutability": "view" }, { "type": "function", - "name": "set_metadata_uri", + "name": "set_metadata", "inputs": [ { - "name": "resource", - "type": "core::felt252" - }, - { - "name": "uri", - "type": "core::array::Span::" + "name": "metadata", + "type": "dojo::resource_metadata::ResourceMetadata" } ], "outputs": [], @@ -120,7 +106,7 @@ test_manifest_file ], "outputs": [ { - "type": "core::starknet::class_hash::ClassHash" + "type": "(core::starknet::class_hash::ClassHash, core::starknet::contract_address::ContractAddress)" } ], "state_mutability": "view" @@ -216,14 +202,6 @@ test_manifest_file "name": "keys", "type": "core::array::Span::" }, - { - "name": "offset", - "type": "core::integer::u8" - }, - { - "name": "length", - "type": "core::integer::u32" - }, { "name": "layout", "type": "core::array::Span::" @@ -248,10 +226,6 @@ test_manifest_file "name": "keys", "type": "core::array::Span::" }, - { - "name": "offset", - "type": "core::integer::u8" - }, { "name": "values", "type": "core::array::Span::" @@ -264,77 +238,6 @@ test_manifest_file "outputs": [], "state_mutability": "external" }, - { - "type": "function", - "name": "entities", - "inputs": [ - { - "name": "model", - "type": "core::felt252" - }, - { - "name": "index", - "type": "core::option::Option::" - }, - { - "name": "values", - "type": "core::array::Span::" - }, - { - "name": "values_length", - "type": "core::integer::u32" - }, - { - "name": "values_layout", - "type": "core::array::Span::" - } - ], - "outputs": [ - { - "type": "(core::array::Span::, core::array::Span::>)" - } - ], - "state_mutability": "view" - }, - { - "type": "function", - "name": "entity_ids", - "inputs": [ - { - "name": "model", - "type": "core::felt252" - } - ], - "outputs": [ - { - "type": "core::array::Span::" - } - ], - "state_mutability": "view" - }, - { - "type": "function", - "name": "set_executor", - "inputs": [ - { - "name": "contract_address", - "type": "core::starknet::contract_address::ContractAddress" - } - ], - "outputs": [], - "state_mutability": "external" - }, - { - "type": "function", - "name": "executor", - "inputs": [], - "outputs": [ - { - "type": "core::starknet::contract_address::ContractAddress" - } - ], - "state_mutability": "view" - }, { "type": "function", "name": "base", @@ -499,10 +402,6 @@ test_manifest_file "type": "constructor", "name": "constructor", "inputs": [ - { - "name": "executor", - "type": "core::starknet::contract_address::ContractAddress" - }, { "name": "contract_base", "type": "core::starknet::class_hash::ClassHash" @@ -613,6 +512,16 @@ test_manifest_file "name": "prev_class_hash", "type": "core::starknet::class_hash::ClassHash", "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "prev_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" } ] }, @@ -631,11 +540,6 @@ test_manifest_file "type": "core::array::Span::", "kind": "data" }, - { - "name": "offset", - "type": "core::integer::u8", - "kind": "data" - }, { "name": "values", "type": "core::array::Span::", @@ -704,23 +608,6 @@ test_manifest_file } ] }, - { - "type": "event", - "name": "dojo::world::world::ExecutorUpdated", - "kind": "struct", - "members": [ - { - "name": "address", - "type": "core::starknet::contract_address::ContractAddress", - "kind": "data" - }, - { - "name": "prev_address", - "type": "core::starknet::contract_address::ContractAddress", - "kind": "data" - } - ] - }, { "type": "event", "name": "dojo::world::world::Event", @@ -775,11 +662,6 @@ test_manifest_file "name": "OwnerUpdated", "type": "dojo::world::world::OwnerUpdated", "kind": "nested" - }, - { - "name": "ExecutorUpdated", - "type": "dojo::world::world::ExecutorUpdated", - "kind": "nested" } ] } @@ -788,70 +670,9 @@ test_manifest_file "writes": [], "computed": [] }, - "executor": { - "name": "dojo::executor::executor", - "address": null, - "class_hash": "0x55865ef05f918b04b08aa1588e9a49e80cbaecdf13e1438c142957e9aedaf73", - "abi": [ - { - "type": "impl", - "name": "Executor", - "interface_name": "dojo::executor::IExecutor" - }, - { - "type": "struct", - "name": "core::array::Span::", - "members": [ - { - "name": "snapshot", - "type": "@core::array::Array::" - } - ] - }, - { - "type": "interface", - "name": "dojo::executor::IExecutor", - "items": [ - { - "type": "function", - "name": "call", - "inputs": [ - { - "name": "class_hash", - "type": "core::starknet::class_hash::ClassHash" - }, - { - "name": "entrypoint", - "type": "core::felt252" - }, - { - "name": "calldata", - "type": "core::array::Span::" - } - ], - "outputs": [ - { - "type": "core::array::Span::" - } - ], - "state_mutability": "view" - } - ] - }, - { - "type": "event", - "name": "dojo::executor::executor::Event", - "kind": "enum", - "variants": [] - } - ], - "reads": [], - "writes": [], - "computed": [] - }, "base": { "name": "dojo::base::base", - "class_hash": "0x48bca802a22c7d7e447cf62c62581acf5f1eae00554d49e2279b0b0132a1777", + "class_hash": "0x794d5ed2f7eb970f92e0ed9be8f73bbbdf18f7db2a9a296fa12c2d9c33e6ab3", "abi": [ { "type": "impl", @@ -951,11 +772,210 @@ test_manifest_file } ] }, + "resource_metadata": { + "name": "dojo::resource_metadata::resource_metadata", + "address": null, + "class_hash": "0x6a2f06cde4aad60e0b6dd595edebe8dca1fbefe5b36cfc2f46a1d1159757df9", + "abi": [ + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "unpacked_size", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "packed_size", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "function", + "name": "layout", + "inputs": [], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "view" + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::>", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::>" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Struct", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "children", + "type": "core::array::Span::>" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::<(core::felt252, core::array::Span::)>", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::<(core::felt252, core::array::Span::)>" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Enum", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "children", + "type": "core::array::Span::<(core::felt252, core::array::Span::)>" + } + ] + }, + { + "type": "enum", + "name": "dojo::database::introspect::Ty", + "variants": [ + { + "name": "Primitive", + "type": "core::felt252" + }, + { + "name": "Struct", + "type": "dojo::database::introspect::Struct" + }, + { + "name": "Enum", + "type": "dojo::database::introspect::Enum" + }, + { + "name": "Tuple", + "type": "core::array::Span::>" + }, + { + "name": "Array", + "type": "core::integer::u32" + } + ] + }, + { + "type": "function", + "name": "schema", + "inputs": [], + "outputs": [ + { + "type": "dojo::database::introspect::Ty" + } + ], + "state_mutability": "view" + }, + { + "type": "struct", + "name": "dojo::resource_metadata::ResourceMetadata", + "members": [ + { + "name": "resource_id", + "type": "core::felt252" + }, + { + "name": "metadata_uri", + "type": "core::array::Span::" + } + ] + }, + { + "type": "function", + "name": "ensure_abi", + "inputs": [ + { + "name": "model", + "type": "dojo::resource_metadata::ResourceMetadata" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "event", + "name": "dojo::resource_metadata::resource_metadata::Event", + "kind": "enum", + "variants": [] + } + ], + "reads": [], + "writes": [], + "computed": [] + }, "contracts": [ { "name": "dojo_examples::actions::actions", "address": null, - "class_hash": "0x2b6fa193f3bc891a7508ebd94ecc57777a576d9b49a84b786aed378176499ca", + "class_hash": "0x2a1c4999d12c32667739532ef820d68ab01db6bed62ea3fd2da08e4d36cca63", "abi": [ { "type": "impl", @@ -1227,7 +1247,7 @@ test_manifest_file { "name": "dojo_examples::models::moves", "address": null, - "class_hash": "0x572a5030f1e283e3cc2ab9e7d5d2d185a43e141fd4606f376691e8abc127d76", + "class_hash": "0x764906a97ff3e532e82b154908b25711cdec1c692bf68e3aba2a3dd9964a15c", "abi": [ { "type": "impl", @@ -1329,6 +1349,10 @@ test_manifest_file { "name": "Tuple", "type": "core::array::Span::>" + }, + { + "name": "Array", + "type": "core::integer::u32" } ] }, @@ -1474,7 +1498,7 @@ test_manifest_file { "name": "dojo_examples::models::position", "address": null, - "class_hash": "0x1e0fd72622c19b91620327ee05e4c226e6d383ec95ba67f2b3830b8c9aa5e4a", + "class_hash": "0x53672d63a83f40ab5f3aeec55d1541a98aa822f5b197a30fbbac28e6f98a7d8", "abi": [ { "type": "impl", @@ -1576,6 +1600,10 @@ test_manifest_file { "name": "Tuple", "type": "core::array::Span::>" + }, + { + "name": "Array", + "type": "core::integer::u32" } ] }, diff --git a/crates/dojo-lang/src/model.rs b/crates/dojo-lang/src/model.rs index 5d4901dda5..608844a2fe 100644 --- a/crates/dojo-lang/src/model.rs +++ b/crates/dojo-lang/src/model.rs @@ -173,7 +173,10 @@ pub fn handle_model_struct( "type_name".to_string(), RewriteNode::new_trimmed(struct_ast.name(db).as_syntax_node()), ), - ("schema_introspection".to_string(), handle_introspect_struct(db, struct_ast)), + ( + "schema_introspection".to_string(), + handle_introspect_struct(db, &mut diagnostics, struct_ast), + ), ("serialized_keys".to_string(), RewriteNode::new_modified(serialized_keys)), ("serialized_values".to_string(), RewriteNode::new_modified(serialized_values)), ]), diff --git a/crates/dojo-lang/src/plugin.rs b/crates/dojo-lang/src/plugin.rs index 2313e7d7ed..9900115cb2 100644 --- a/crates/dojo-lang/src/plugin.rs +++ b/crates/dojo-lang/src/plugin.rs @@ -23,6 +23,7 @@ use smol_str::SmolStr; use url::Url; use crate::contract::DojoContract; +use crate::inline_macros::array_cap::ArrayCapMacro; use crate::inline_macros::delete::DeleteMacro; use crate::inline_macros::emit::EmitMacro; use crate::inline_macros::get::GetMacro; @@ -32,6 +33,7 @@ use crate::model::handle_model_struct; use crate::print::{handle_print_enum, handle_print_struct}; const DOJO_CONTRACT_ATTR: &str = "dojo::contract"; +const DOJO_PLUGIN_EXPAND_VAR_ENV: &str = "DOJO_PLUGIN_EXPAND"; #[derive(Clone, Debug, PartialEq)] pub struct Model { @@ -224,6 +226,7 @@ pub fn dojo_plugin_suite() -> PluginSuite { .add_inline_macro_plugin::() .add_inline_macro_plugin::() .add_inline_macro_plugin::() + .add_inline_macro_plugin::() .add_inline_macro_plugin::(); suite @@ -238,6 +241,9 @@ impl MacroPlugin for BuiltinDojoPlugin { item_ast: ast::ModuleItem, _metadata: &MacroPluginMetadata<'_>, ) -> PluginResult { + let do_expand: bool = + std::env::var(DOJO_PLUGIN_EXPAND_VAR_ENV).map_or(false, |v| v == "true" || v == "1"); + match item_ast { ast::ModuleItem::Module(module_ast) => self.handle_mod(db, module_ast), ast::ModuleItem::Enum(enum_ast) => { @@ -308,6 +314,10 @@ impl MacroPlugin for BuiltinDojoPlugin { builder.add_modified(node); } + if do_expand { + println!("{}", builder.code); + } + PluginResult { code: Some(PluginGeneratedFile { name, @@ -374,8 +384,11 @@ impl MacroPlugin for BuiltinDojoPlugin { rewrite_nodes.push(handle_print_struct(db, struct_ast.clone())); } "Introspect" => { - rewrite_nodes - .push(handle_introspect_struct(db, struct_ast.clone())); + rewrite_nodes.push(handle_introspect_struct( + db, + &mut diagnostics, + struct_ast.clone(), + )); } _ => continue, } @@ -392,6 +405,10 @@ impl MacroPlugin for BuiltinDojoPlugin { builder.add_modified(node); } + if do_expand { + println!("{}", builder.code); + } + PluginResult { code: Some(PluginGeneratedFile { name, @@ -409,7 +426,14 @@ impl MacroPlugin for BuiltinDojoPlugin { } fn declared_attributes(&self) -> Vec { - vec!["dojo::contract".to_string(), "key".to_string(), "computed".to_string()] + vec![ + "dojo::contract".to_string(), + "key".to_string(), + "computed".to_string(), + // Not adding capacity for now, this will automatically + // makes Scarb emitting a diagnostic saying this attribute is not supported. + // "capacity".to_string(), + ] } } diff --git a/crates/dojo-lang/src/plugin_test_data/introspect b/crates/dojo-lang/src/plugin_test_data/introspect index 7eac81934c..210ad28b58 100644 --- a/crates/dojo-lang/src/plugin_test_data/introspect +++ b/crates/dojo-lang/src/plugin_test_data/introspect @@ -50,6 +50,24 @@ struct GenericStruct { t: T, } +#[derive(Copy, Drop, Serde, Introspect)] +struct FeltsArray { + #[capacity(10)] + felts: Array, +} + +#[derive(Copy, Drop, Serde, Introspect)] +struct CapacityInvalidType { + #[capacity(10)] + value: Position, +} + +#[derive(Copy, Drop, Serde, Introspect)] +struct FeltsArrayBadCapacity { + #[capacity(0)] + felts: Array, +} + //! > expanded_cairo_code use core::serde::Serde; @@ -96,6 +114,24 @@ struct Position { struct GenericStruct { t: T, } + +#[derive(Copy, Drop, Serde, Introspect)] +struct FeltsArray { + #[capacity(10)] + felts: Array, +} + +#[derive(Copy, Drop, Serde, Introspect)] +struct CapacityInvalidType { + #[capacity(10)] + value: Position, +} + +#[derive(Copy, Drop, Serde, Introspect)] +struct FeltsArrayBadCapacity { + #[capacity(0)] + felts: Array, +} impl Vec2Copy of core::traits::Copy::; impl Vec2Drop of core::traits::Drop::; impl Vec2Serde of core::serde::Serde:: { @@ -453,5 +489,163 @@ impl GenericStructIntrospect> of core::traits::Copy::>; +impl FeltsArrayDrop> of core::traits::Drop::>; +impl FeltsArraySerde, +core::traits::Destruct> of core::serde::Serde::> { + fn serialize(self: @FeltsArray, ref output: core::array::Array) { + core::serde::Serde::serialize(self.felts, ref output) + } + fn deserialize(ref serialized: core::array::Span) -> core::option::Option> { + core::option::Option::Some(FeltsArray { + felts: core::serde::Serde::deserialize(ref serialized)?, + }) + } +} + +impl FeltsArrayIntrospect> of dojo::database::introspect::Introspect> { + #[inline(always)] + fn size() -> usize { + 1 + 10 + 0 + } + + #[inline(always)] + fn layout(ref layout: Array) { + layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); + + } + + #[inline(always)] + fn ty() -> dojo::database::introspect::Ty { + dojo::database::introspect::Ty::Struct(dojo::database::introspect::Struct { + name: 'FeltsArray', + attrs: array![].span(), + children: array![dojo::database::introspect::serialize_member(@dojo::database::introspect::Member { + name: 'felts', + ty: dojo::database::introspect::Ty::Array(10), + attrs: array![].span() + })].span() + }) + } +} +impl CapacityInvalidTypeCopy> of core::traits::Copy::>; +impl CapacityInvalidTypeDrop> of core::traits::Drop::>; +impl CapacityInvalidTypeSerde, +core::traits::Destruct> of core::serde::Serde::> { + fn serialize(self: @CapacityInvalidType, ref output: core::array::Array) { + core::serde::Serde::serialize(self.value, ref output) + } + fn deserialize(ref serialized: core::array::Span) -> core::option::Option> { + core::option::Option::Some(CapacityInvalidType { + value: core::serde::Serde::deserialize(ref serialized)?, + }) + } +} + +impl CapacityInvalidTypeIntrospect> of dojo::database::introspect::Introspect> { + #[inline(always)] + fn size() -> usize { + 1 + 10 + 0 + } + + #[inline(always)] + fn layout(ref layout: Array) { + layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); +layout.append(251); + + } + + #[inline(always)] + fn ty() -> dojo::database::introspect::Ty { + dojo::database::introspect::Ty::Struct(dojo::database::introspect::Struct { + name: 'CapacityInvalidType', + attrs: array![].span(), + children: array![dojo::database::introspect::serialize_member(@dojo::database::introspect::Member { + name: 'value', + ty: dojo::database::introspect::Ty::Array(10), + attrs: array![].span() + })].span() + }) + } +} +impl FeltsArrayBadCapacityCopy> of core::traits::Copy::>; +impl FeltsArrayBadCapacityDrop> of core::traits::Drop::>; +impl FeltsArrayBadCapacitySerde, +core::traits::Destruct> of core::serde::Serde::> { + fn serialize(self: @FeltsArrayBadCapacity, ref output: core::array::Array) { + core::serde::Serde::serialize(self.felts, ref output) + } + fn deserialize(ref serialized: core::array::Span) -> core::option::Option> { + core::option::Option::Some(FeltsArrayBadCapacity { + felts: core::serde::Serde::deserialize(ref serialized)?, + }) + } +} + +impl FeltsArrayBadCapacityIntrospect> of dojo::database::introspect::Introspect> { + #[inline(always)] + fn size() -> usize { + 1 + 0 + 0 + } + + #[inline(always)] + fn layout(ref layout: Array) { + layout.append(251); + + } + + #[inline(always)] + fn ty() -> dojo::database::introspect::Ty { + dojo::database::introspect::Ty::Struct(dojo::database::introspect::Struct { + name: 'FeltsArrayBadCapacity', + attrs: array![].span(), + children: array![dojo::database::introspect::serialize_member(@dojo::database::introspect::Member { + name: 'felts', + ty: dojo::database::introspect::Ty::Array(0), + attrs: array![].span() + })].span() + }) + } +} //! > expected_diagnostics +error: Unsupported attribute. + --> test_src/lib.cairo:49:5 + #[capacity(10)] + ^*************^ + +error: Capacity is only supported for Array or Span. + --> test_src/lib.cairo:55:5 + #[capacity(10)] + ^*************^ + +error: Unsupported attribute. + --> test_src/lib.cairo:55:5 + #[capacity(10)] + ^*************^ + +error: Capacity must be greater than 0. + --> test_src/lib.cairo:61:5 + #[capacity(0)] + ^************^ + +error: Unsupported attribute. + --> test_src/lib.cairo:61:5 + #[capacity(0)] + ^************^ diff --git a/crates/dojo-lang/src/semantics/test_data/array_cap b/crates/dojo-lang/src/semantics/test_data/array_cap new file mode 100644 index 0000000000..9cd17d8c22 --- /dev/null +++ b/crates/dojo-lang/src/semantics/test_data/array_cap @@ -0,0 +1,151 @@ +//! > no params + +//! > test_runner_name +test_semantics + +//! > expression +array_cap!() + +//! > expected +Missing( + ExprMissing { + ty: , + }, +) + +//! > semantic_diagnostics +error: Plugin diagnostic: Invalid arguments. Expected "(capacity, (values,))" + --> lib.cairo:2:12 +array_cap!() + ^ + +//! > ========================================================================== + +//! > Capacity exceeded + +//! > test_runner_name +test_semantics + +//! > expression +array_cap!(1, (1, 2, 3)) + +//! > expected +Missing( + ExprMissing { + ty: , + }, +) + +//! > semantic_diagnostics +error: Plugin diagnostic: Number of values is exceeded the capacity + --> lib.cairo:2:22 +array_cap!(1, (1, 2, 3)) + ^ + +//! > ========================================================================== + +//! > set successful expansion + +//! > no_diagnostics +true + +//! > test_runner_name +test_semantics + +//! > expression +array_cap!(1, (0xfe, 2)) + +//! > expected +Block( + ExprBlock { + statements: [ + Let( + StatementLet { + pattern: Variable( + __array_with_cap__, + ), + expr: Block( + ExprBlock { + statements: [ + Let( + StatementLet { + pattern: Variable( + __array_builder_macro_result__, + ), + expr: FunctionCall( + ExprFunctionCall { + function: core::array::ArrayImpl::::new, + args: [], + ty: core::array::Array::, + }, + ), + }, + ), + ], + tail: Some( + Var( + LocalVarId(test::__array_builder_macro_result__), + ), + ), + ty: core::array::Array::, + }, + ), + }, + ), + Expr( + StatementExpr { + expr: FunctionCall( + ExprFunctionCall { + function: core::array::ArrayImpl::::append, + args: [ + Reference( + LocalVarId(test::__array_with_cap__), + ), + Value( + Literal( + ExprLiteral { + value: 254, + ty: core::felt252, + }, + ), + ), + ], + ty: (), + }, + ), + }, + ), + Expr( + StatementExpr { + expr: FunctionCall( + ExprFunctionCall { + function: core::array::ArrayImpl::::append, + args: [ + Reference( + LocalVarId(test::__array_with_cap__), + ), + Value( + Literal( + ExprLiteral { + value: 2, + ty: core::felt252, + }, + ), + ), + ], + ty: (), + }, + ), + }, + ), + ], + tail: Some( + Var( + LocalVarId(test::__array_with_cap__), + ), + ), + ty: core::array::Array::, + }, +) + +//! > semantic_diagnostics diff --git a/crates/dojo-lang/src/semantics/test_data/get b/crates/dojo-lang/src/semantics/test_data/get index 3a4f5d0b33..04d1b0419a 100644 --- a/crates/dojo-lang/src/semantics/test_data/get +++ b/crates/dojo-lang/src/semantics/test_data/get @@ -322,27 +322,6 @@ Block( LocalVarId(test::__get_macro_keys__), ), ), - Value( - Literal( - ExprLiteral { - value: 0, - ty: core::integer::u8, - }, - ), - ), - Value( - FunctionCall( - ExprFunctionCall { - function: dojo::packing::calculate_packed_size, - args: [ - Reference( - LocalVarId(test::__Health_layout_clone_span__), - ), - ], - ty: core::integer::u32, - }, - ), - ), Value( Var( LocalVarId(test::__Health_layout_span__), diff --git a/crates/dojo-lang/src/semantics/test_data/set b/crates/dojo-lang/src/semantics/test_data/set index 73517cab9a..8f62ef682d 100644 --- a/crates/dojo-lang/src/semantics/test_data/set +++ b/crates/dojo-lang/src/semantics/test_data/set @@ -206,14 +206,6 @@ Block( }, ), ), - Value( - Literal( - ExprLiteral { - value: 0, - ty: core::integer::u8, - }, - ), - ), Value( FunctionCall( ExprFunctionCall { diff --git a/crates/dojo-lang/src/semantics/tests.rs b/crates/dojo-lang/src/semantics/tests.rs index 604c87e6d0..3449b4a5ae 100644 --- a/crates/dojo-lang/src/semantics/tests.rs +++ b/crates/dojo-lang/src/semantics/tests.rs @@ -16,6 +16,8 @@ test_file_test!( get: "get", set: "set", + + array_cap: "array_cap", }, test_semantics ); diff --git a/crates/dojo-test-utils/build.rs b/crates/dojo-test-utils/build.rs index f80c57027d..827d4bd0a2 100644 --- a/crates/dojo-test-utils/build.rs +++ b/crates/dojo-test-utils/build.rs @@ -15,6 +15,11 @@ fn main() { project_paths.iter().for_each(|path| compile(path)); + println!("cargo:rerun-if-changed=../../examples"); + println!("cargo:rerun-if-changed=../torii/types-test"); + println!("cargo:rerun-if-changed=../dojo-lang/src"); + println!("cargo:rerun-if-changed=../../bin/sozo/src"); + fn compile(path: &str) { let target_path = Utf8PathBuf::from_path_buf(format!("{}/target", path).into()).unwrap(); if target_path.exists() { diff --git a/crates/dojo-types/src/lib.rs b/crates/dojo-types/src/lib.rs index 757994b8bb..bb73984aa1 100644 --- a/crates/dojo-types/src/lib.rs +++ b/crates/dojo-types/src/lib.rs @@ -16,8 +16,6 @@ pub mod system; pub struct WorldMetadata { pub world_address: FieldElement, pub world_class_hash: FieldElement, - pub executor_address: FieldElement, - pub executor_class_hash: FieldElement, pub models: HashMap, } diff --git a/crates/dojo-types/src/schema.rs b/crates/dojo-types/src/schema.rs index a1e42206b7..8e2a5ac2a9 100644 --- a/crates/dojo-types/src/schema.rs +++ b/crates/dojo-types/src/schema.rs @@ -27,6 +27,7 @@ pub struct ModelMetadata { pub packed_size: u32, pub unpacked_size: u32, pub class_hash: FieldElement, + pub contract_address: FieldElement, pub layout: Vec, } diff --git a/crates/dojo-world/Cargo.toml b/crates/dojo-world/Cargo.toml index c44fea5a45..7582c2f12f 100644 --- a/crates/dojo-world/Cargo.toml +++ b/crates/dojo-world/Cargo.toml @@ -24,7 +24,7 @@ starknet.workspace = true thiserror.workspace = true tracing.workspace = true -cainome = { git = "https://github.com/cartridge-gg/cainome", rev = "950e487", features = [ "abigen-rs" ] } +cainome = { git = "https://github.com/cartridge-gg/cainome", tag = "v0.2.4", features = ["abigen-rs"] } dojo-types = { path = "../dojo-types", optional = true } http = { version = "0.2.9", optional = true } ipfs-api-backend-hyper = { git = "https://github.com/ferristseng/rust-ipfs-api", rev = "af2c17f7b19ef5b9898f458d97a90055c3605633", features = [ "with-hyper-rustls" ], optional = true } diff --git a/crates/dojo-world/abigen/src/main.rs b/crates/dojo-world/abigen/src/main.rs index f7d00521cc..de79b0bfa0 100644 --- a/crates/dojo-world/abigen/src/main.rs +++ b/crates/dojo-world/abigen/src/main.rs @@ -13,7 +13,8 @@ const SCARB_MANIFEST_BACKUP: &str = "crates/dojo-core/bak.Scarb.toml"; const SCARB_LOCK: &str = "crates/dojo-core/Scarb.lock"; const SCARB_LOCK_BACKUP: &str = "crates/dojo-core/bak.Scarb.lock"; const WORLD_ARTIFACT: &str = "crates/dojo-core/target/dev/dojo_world.contract_class.json"; -const EXECUTOR_ARTIFACT: &str = "crates/dojo-core/target/dev/dojo_executor.contract_class.json"; +const MODEL_ARTIFACT: &str = + "crates/dojo-core/target/dev/dojo_resource_metadata.contract_class.json"; const OUT_DIR: &str = "crates/dojo-world/src/contracts/abi"; fn define_check_only() -> bool { @@ -28,7 +29,7 @@ fn main() { compile_dojo_core(); generate_bindings("WorldContract", WORLD_ARTIFACT, "world.rs", is_check_only); - generate_bindings("ExecutorContract", EXECUTOR_ARTIFACT, "executor.rs", is_check_only); + generate_bindings("ModelContract", MODEL_ARTIFACT, "model.rs", is_check_only); } /// Generates the bindings for the given contracts, or verifies diff --git a/crates/dojo-world/src/contracts/abi/mod.rs b/crates/dojo-world/src/contracts/abi/mod.rs index 2890b5d8e2..3a145dcfd5 100644 --- a/crates/dojo-world/src/contracts/abi/mod.rs +++ b/crates/dojo-world/src/contracts/abi/mod.rs @@ -1,2 +1,3 @@ pub mod executor; +pub mod model; pub mod world; diff --git a/crates/dojo-world/src/contracts/abi/model.rs b/crates/dojo-world/src/contracts/abi/model.rs new file mode 100644 index 0000000000..40ac6cd3c8 --- /dev/null +++ b/crates/dojo-world/src/contracts/abi/model.rs @@ -0,0 +1,198 @@ +// AUTOGENERATED FILE, DO NOT EDIT. +// To generate the bindings, please run `cargo run --bin dojo-world-abigen` instead. +use cainome::rs::abigen; + +abigen!( + ModelContract, + r#"[ + { + "type": "function", + "name": "name", + "inputs": [], + "outputs": [ + { + "type": "core::felt252" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "unpacked_size", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "packed_size", + "inputs": [], + "outputs": [ + { + "type": "core::integer::u32" + } + ], + "state_mutability": "view" + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "function", + "name": "layout", + "inputs": [], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "view" + }, + { + "type": "struct", + "name": "core::array::Span::", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::>", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::>" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Struct", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "children", + "type": "core::array::Span::>" + } + ] + }, + { + "type": "struct", + "name": "core::array::Span::<(core::felt252, core::array::Span::)>", + "members": [ + { + "name": "snapshot", + "type": "@core::array::Array::<(core::felt252, core::array::Span::)>" + } + ] + }, + { + "type": "struct", + "name": "dojo::database::introspect::Enum", + "members": [ + { + "name": "name", + "type": "core::felt252" + }, + { + "name": "attrs", + "type": "core::array::Span::" + }, + { + "name": "children", + "type": "core::array::Span::<(core::felt252, core::array::Span::)>" + } + ] + }, + { + "type": "enum", + "name": "dojo::database::introspect::Ty", + "variants": [ + { + "name": "Primitive", + "type": "core::felt252" + }, + { + "name": "Struct", + "type": "dojo::database::introspect::Struct" + }, + { + "name": "Enum", + "type": "dojo::database::introspect::Enum" + }, + { + "name": "Tuple", + "type": "core::array::Span::>" + }, + { + "name": "Array", + "type": "core::integer::u32" + } + ] + }, + { + "type": "function", + "name": "schema", + "inputs": [], + "outputs": [ + { + "type": "dojo::database::introspect::Ty" + } + ], + "state_mutability": "view" + }, + { + "type": "struct", + "name": "dojo::resource_metadata::ResourceMetadata", + "members": [ + { + "name": "resource_id", + "type": "core::felt252" + }, + { + "name": "metadata_uri", + "type": "core::array::Span::" + } + ] + }, + { + "type": "function", + "name": "ensure_abi", + "inputs": [ + { + "name": "model", + "type": "dojo::resource_metadata::ResourceMetadata" + } + ], + "outputs": [], + "state_mutability": "view" + }, + { + "type": "event", + "name": "dojo::resource_metadata::resource_metadata::Event", + "kind": "enum", + "variants": [] + } +]"# +); diff --git a/crates/dojo-world/src/contracts/abi/world.rs b/crates/dojo-world/src/contracts/abi/world.rs index de66ca66b0..af7b3cacf8 100644 --- a/crates/dojo-world/src/contracts/abi/world.rs +++ b/crates/dojo-world/src/contracts/abi/world.rs @@ -22,35 +22,25 @@ abigen!( }, { "type": "struct", - "name": "core::array::Span::", + "name": "dojo::resource_metadata::ResourceMetadata", "members": [ { - "name": "snapshot", - "type": "@core::array::Array::" - } - ] - }, - { - "type": "enum", - "name": "core::option::Option::", - "variants": [ - { - "name": "Some", + "name": "resource_id", "type": "core::felt252" }, { - "name": "None", - "type": "()" + "name": "metadata_uri", + "type": "core::array::Span::" } ] }, { "type": "struct", - "name": "core::array::Span::>", + "name": "core::array::Span::", "members": [ { "name": "snapshot", - "type": "@core::array::Array::>" + "type": "@core::array::Array::" } ] }, @@ -74,31 +64,27 @@ abigen!( "items": [ { "type": "function", - "name": "metadata_uri", + "name": "metadata", "inputs": [ { - "name": "resource", + "name": "resource_id", "type": "core::felt252" } ], "outputs": [ { - "type": "core::array::Span::" + "type": "dojo::resource_metadata::ResourceMetadata" } ], "state_mutability": "view" }, { "type": "function", - "name": "set_metadata_uri", + "name": "set_metadata", "inputs": [ { - "name": "resource", - "type": "core::felt252" - }, - { - "name": "uri", - "type": "core::array::Span::" + "name": "metadata", + "type": "dojo::resource_metadata::ResourceMetadata" } ], "outputs": [], @@ -115,7 +101,7 @@ abigen!( ], "outputs": [ { - "type": "core::starknet::class_hash::ClassHash" + "type": "(core::starknet::class_hash::ClassHash, core::starknet::contract_address::ContractAddress)" } ], "state_mutability": "view" @@ -211,14 +197,6 @@ abigen!( "name": "keys", "type": "core::array::Span::" }, - { - "name": "offset", - "type": "core::integer::u8" - }, - { - "name": "length", - "type": "core::integer::u32" - }, { "name": "layout", "type": "core::array::Span::" @@ -243,10 +221,6 @@ abigen!( "name": "keys", "type": "core::array::Span::" }, - { - "name": "offset", - "type": "core::integer::u8" - }, { "name": "values", "type": "core::array::Span::" @@ -259,77 +233,6 @@ abigen!( "outputs": [], "state_mutability": "external" }, - { - "type": "function", - "name": "entities", - "inputs": [ - { - "name": "model", - "type": "core::felt252" - }, - { - "name": "index", - "type": "core::option::Option::" - }, - { - "name": "values", - "type": "core::array::Span::" - }, - { - "name": "values_length", - "type": "core::integer::u32" - }, - { - "name": "values_layout", - "type": "core::array::Span::" - } - ], - "outputs": [ - { - "type": "(core::array::Span::, core::array::Span::>)" - } - ], - "state_mutability": "view" - }, - { - "type": "function", - "name": "entity_ids", - "inputs": [ - { - "name": "model", - "type": "core::felt252" - } - ], - "outputs": [ - { - "type": "core::array::Span::" - } - ], - "state_mutability": "view" - }, - { - "type": "function", - "name": "set_executor", - "inputs": [ - { - "name": "contract_address", - "type": "core::starknet::contract_address::ContractAddress" - } - ], - "outputs": [], - "state_mutability": "external" - }, - { - "type": "function", - "name": "executor", - "inputs": [], - "outputs": [ - { - "type": "core::starknet::contract_address::ContractAddress" - } - ], - "state_mutability": "view" - }, { "type": "function", "name": "base", @@ -494,10 +397,6 @@ abigen!( "type": "constructor", "name": "constructor", "inputs": [ - { - "name": "executor", - "type": "core::starknet::contract_address::ContractAddress" - }, { "name": "contract_base", "type": "core::starknet::class_hash::ClassHash" @@ -608,6 +507,16 @@ abigen!( "name": "prev_class_hash", "type": "core::starknet::class_hash::ClassHash", "kind": "data" + }, + { + "name": "address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" + }, + { + "name": "prev_address", + "type": "core::starknet::contract_address::ContractAddress", + "kind": "data" } ] }, @@ -626,11 +535,6 @@ abigen!( "type": "core::array::Span::", "kind": "data" }, - { - "name": "offset", - "type": "core::integer::u8", - "kind": "data" - }, { "name": "values", "type": "core::array::Span::", @@ -699,23 +603,6 @@ abigen!( } ] }, - { - "type": "event", - "name": "dojo::world::world::ExecutorUpdated", - "kind": "struct", - "members": [ - { - "name": "address", - "type": "core::starknet::contract_address::ContractAddress", - "kind": "data" - }, - { - "name": "prev_address", - "type": "core::starknet::contract_address::ContractAddress", - "kind": "data" - } - ] - }, { "type": "event", "name": "dojo::world::world::Event", @@ -770,11 +657,6 @@ abigen!( "name": "OwnerUpdated", "type": "dojo::world::world::OwnerUpdated", "kind": "nested" - }, - { - "name": "ExecutorUpdated", - "type": "dojo::world::world::ExecutorUpdated", - "kind": "nested" } ] } diff --git a/crates/dojo-world/src/contracts/mod.rs b/crates/dojo-world/src/contracts/mod.rs index 8200eb17a6..9174da838a 100644 --- a/crates/dojo-world/src/contracts/mod.rs +++ b/crates/dojo-world/src/contracts/mod.rs @@ -1,5 +1,4 @@ -mod abi; - +pub mod abi; pub mod cairo_utils; pub mod model; pub mod world; diff --git a/crates/dojo-world/src/contracts/model.rs b/crates/dojo-world/src/contracts/model.rs index b0ac6e37ed..e36b2962a2 100644 --- a/crates/dojo-world/src/contracts/model.rs +++ b/crates/dojo-world/src/contracts/model.rs @@ -1,5 +1,4 @@ -use std::vec; - +pub use abigen::model::ModelContractReader; use async_trait::async_trait; use cainome::cairo_serde::Error as CainomeError; use dojo_types::packing::{parse_ty, unpack, PackingError, ParseError}; @@ -7,8 +6,7 @@ use dojo_types::primitive::PrimitiveError; use dojo_types::schema::Ty; use starknet::core::types::{FieldElement, StarknetError}; use starknet::core::utils::{ - cairo_short_string_to_felt, get_selector_from_name, CairoShortStringToFeltError, - ParseCairoShortStringError, + cairo_short_string_to_felt, CairoShortStringToFeltError, ParseCairoShortStringError, }; use starknet::macros::short_string; use starknet::providers::{Provider, ProviderError}; @@ -16,15 +14,16 @@ use starknet_crypto::poseidon_hash_many; use crate::contracts::WorldContractReader; -const SCHEMA_SELECTOR_STR: &str = "schema"; -const LAYOUT_SELECTOR_STR: &str = "layout"; -const PACKED_SIZE_SELECTOR_STR: &str = "packed_size"; -const UNPACKED_SIZE_SELECTOR_STR: &str = "unpacked_size"; - #[cfg(test)] #[path = "model_test.rs"] mod model_test; +pub mod abigen { + pub mod model { + pub use crate::contracts::abi::model::*; + } +} + #[derive(Debug, thiserror::Error)] pub enum ModelError { #[error("Model not found.")] @@ -49,6 +48,7 @@ pub enum ModelError { #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] pub trait ModelReader { fn class_hash(&self) -> FieldElement; + fn contract_address(&self) -> FieldElement; async fn schema(&self) -> Result; async fn packed_size(&self) -> Result; async fn unpacked_size(&self) -> Result; @@ -60,8 +60,12 @@ pub struct ModelRPCReader<'a, P: Provider + Sync + Send> { name: FieldElement, /// The class hash of the model class_hash: FieldElement, + /// The contract address of the model + contract_address: FieldElement, /// Contract reader of the World that the model is registered to. world_reader: &'a WorldContractReader

, + /// Contract reader of the model. + model_reader: ModelContractReader<&'a P>, } impl<'a, P> ModelRPCReader<'a, P> @@ -74,7 +78,7 @@ where ) -> Result, ModelError> { let name = cairo_short_string_to_felt(name)?; - let class_hash = + let (class_hash, contract_address) = world.model(&name).block_id(world.block_id).call().await.map_err(|err| match err { CainomeError::Provider(ProviderError::StarknetError( StarknetError::ContractNotFound, @@ -82,7 +86,15 @@ where err => err.into(), })?; - Ok(Self { world_reader: world, class_hash: class_hash.into(), name }) + let model_reader = ModelContractReader::new(contract_address.into(), world.provider()); + + Ok(Self { + world_reader: world, + class_hash: class_hash.into(), + contract_address: contract_address.into(), + name, + model_reader, + }) } pub async fn entity_storage( @@ -138,40 +150,29 @@ where self.class_hash } - async fn schema(&self) -> Result { - let entrypoint = get_selector_from_name(SCHEMA_SELECTOR_STR).unwrap(); - - let res = self.world_reader.executor_call(self.class_hash, entrypoint, vec![]).await?; + fn contract_address(&self) -> FieldElement { + self.contract_address + } + async fn schema(&self) -> Result { + let res = self.model_reader.schema().raw_call().await?; Ok(parse_ty(&res)?) } async fn packed_size(&self) -> Result { - let entrypoint = get_selector_from_name(PACKED_SIZE_SELECTOR_STR).unwrap(); - - let res = self.world_reader.executor_call(self.class_hash, entrypoint, vec![]).await?; - - Ok(res[0]) + Ok(self.model_reader.packed_size().raw_call().await?[0]) } async fn unpacked_size(&self) -> Result { - let entrypoint = get_selector_from_name(UNPACKED_SIZE_SELECTOR_STR).unwrap(); - - let res = self.world_reader.executor_call(self.class_hash, entrypoint, vec![]).await?; - - Ok(res[0]) + Ok(self.model_reader.unpacked_size().raw_call().await?[0]) } async fn layout(&self) -> Result, ModelError> { - let entrypoint = get_selector_from_name(LAYOUT_SELECTOR_STR).unwrap(); - - let res = self.world_reader.executor_call(self.class_hash, entrypoint, vec![]).await?; - // Layout entrypoint expanded by the #[model] attribute returns a // `Span`. So cainome generated code will deserialize the result // of `executor.call()` which is a Vec. // So inside the vec, we skip the first element, which is the length // of the span returned by `layout` entrypoint of the model code. - Ok(res[1..].into()) + Ok(self.model_reader.layout().raw_call().await?[1..].into()) } } diff --git a/crates/dojo-world/src/contracts/model_test.rs b/crates/dojo-world/src/contracts/model_test.rs index 99adb762cc..e797b2e96e 100644 --- a/crates/dojo-world/src/contracts/model_test.rs +++ b/crates/dojo-world/src/contracts/model_test.rs @@ -17,7 +17,7 @@ async fn test_model() { TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; let account = sequencer.account(); let provider = account.provider(); - let (world_address, _) = deploy_world( + let world_address = deploy_world( &sequencer, Utf8PathBuf::from_path_buf("../../examples/spawn-and-move/target/dev".into()).unwrap(), ) @@ -63,7 +63,7 @@ async fn test_model() { assert_eq!( position.class_hash(), FieldElement::from_hex_be( - "0x01e0fd72622c19b91620327ee05e4c226e6d383ec95ba67f2b3830b8c9aa5e4a" + "0x053672d63a83f40ab5f3aeec55d1541a98aa822f5b197a30fbbac28e6f98a7d8" ) .unwrap() ); diff --git a/crates/dojo-world/src/contracts/world.rs b/crates/dojo-world/src/contracts/world.rs index 6f601c2236..8a639c421c 100644 --- a/crates/dojo-world/src/contracts/world.rs +++ b/crates/dojo-world/src/contracts/world.rs @@ -1,8 +1,9 @@ use std::result::Result; -pub use abigen::world::{WorldContract, WorldContractReader}; -use cainome::cairo_serde::Result as CainomeResult; -use starknet::core::types::FieldElement; +pub use abigen::world::{ + ContractDeployed, ContractUpgraded, Event as WorldEvent, ModelRegistered, WorldContract, + WorldContractReader, +}; use starknet::providers::Provider; use super::model::{ModelError, ModelRPCReader}; @@ -15,10 +16,6 @@ pub mod abigen { pub mod world { pub use crate::contracts::abi::world::*; } - - pub mod executor { - pub use crate::contracts::abi::executor::*; - } } impl

WorldContractReader

@@ -29,28 +26,3 @@ where ModelRPCReader::new(name, self).await } } - -impl

WorldContractReader

-where - P: Provider + Sync + Send, -{ - pub async fn executor_call( - &self, - class_hash: FieldElement, - entry_point: FieldElement, - calldata: Vec, - ) -> CainomeResult> { - let executor_address = self.executor().block_id(self.block_id).call().await?; - - let executor = - abigen::executor::ExecutorContractReader::new(executor_address.into(), &self.provider); - - let res = executor - .call(&class_hash.into(), &entry_point, &calldata) - .block_id(self.block_id) - .call() - .await?; - - Ok(res) - } -} diff --git a/crates/dojo-world/src/contracts/world_test.rs b/crates/dojo-world/src/contracts/world_test.rs index 2028fad8b8..caa06347f4 100644 --- a/crates/dojo-world/src/contracts/world_test.rs +++ b/crates/dojo-world/src/contracts/world_test.rs @@ -19,22 +19,16 @@ async fn test_world_contract_reader() { TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; let account = sequencer.account(); let provider = account.provider(); - let (world_address, executor_address) = deploy_world( + let world_address = deploy_world( &sequencer, Utf8PathBuf::from_path_buf("../../examples/spawn-and-move/target/dev".into()).unwrap(), ) .await; - let world = WorldContractReader::new(world_address, provider); - let executor = world.executor().call().await.unwrap(); - - assert_eq!(FieldElement::from(executor), executor_address); + let _world = WorldContractReader::new(world_address, provider); } -pub async fn deploy_world( - sequencer: &TestSequencer, - path: Utf8PathBuf, -) -> (FieldElement, FieldElement) { +pub async fn deploy_world(sequencer: &TestSequencer, path: Utf8PathBuf) -> FieldElement { let manifest = Manifest::load_from_path(path.join("manifest.json")).unwrap(); let world = WorldDiff::compute(manifest.clone(), None); let account = sequencer.account(); @@ -46,13 +40,6 @@ pub async fn deploy_world( world, ) .unwrap(); - let executor_address = strategy - .executor - .unwrap() - .deploy(manifest.clone().executor.class_hash, vec![], &account, Default::default()) - .await - .unwrap() - .contract_address; let base_class_hash = strategy.base.unwrap().declare(&account, Default::default()).await.unwrap().class_hash; @@ -65,7 +52,7 @@ pub async fn deploy_world( .unwrap() .deploy( manifest.clone().world.class_hash, - vec![executor_address, base_class_hash], + vec![base_class_hash], &account, Default::default(), ) @@ -105,5 +92,5 @@ pub async fn deploy_world( // wait for the tx to be mined tokio::time::sleep(Duration::from_millis(250)).await; - (world_address, executor_address) + world_address } diff --git a/crates/dojo-world/src/manifest.rs b/crates/dojo-world/src/manifest.rs index 75dfac5135..f933c48fc8 100644 --- a/crates/dojo-world/src/manifest.rs +++ b/crates/dojo-world/src/manifest.rs @@ -3,7 +3,7 @@ use std::fs; use std::path::Path; use ::serde::{Deserialize, Serialize}; -use cainome::cairo_serde::Error as CainomeError; +use cainome::cairo_serde::{ContractAddress, Error as CainomeError}; use cairo_lang_starknet::abi; use serde_with::serde_as; use smol_str::SmolStr; @@ -20,6 +20,7 @@ use starknet::providers::{Provider, ProviderError}; use thiserror::Error; use crate::contracts::model::ModelError; +use crate::contracts::world::WorldEvent; use crate::contracts::WorldContractReader; #[cfg(test)] @@ -27,15 +28,14 @@ use crate::contracts::WorldContractReader; mod test; pub const WORLD_CONTRACT_NAME: &str = "dojo::world::world"; -pub const EXECUTOR_CONTRACT_NAME: &str = "dojo::executor::executor"; pub const BASE_CONTRACT_NAME: &str = "dojo::base::base"; +pub const RESOURCE_METADATA_CONTRACT_NAME: &str = "dojo::resource_metadata::resource_metadata"; +pub const RESOURCE_METADATA_MODEL_NAME: &str = "0x5265736f757263654d65746164617461"; #[derive(Error, Debug)] pub enum ManifestError { #[error("Remote World not found.")] RemoteWorldNotFound, - #[error("Executor contract not found.")] - ExecutorNotFound, #[error("Entry point name contains non-ASCII characters.")] InvalidEntryPointError, #[error(transparent)] @@ -130,8 +130,8 @@ pub struct Class { #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] pub struct Manifest { pub world: Contract, - pub executor: Contract, pub base: Class, + pub resource_metadata: Contract, pub contracts: Vec, pub models: Vec, } @@ -174,19 +174,20 @@ impl Manifest { let world = WorldContractReader::new(world_address, provider); - let executor_address = world.executor().block_id(BLOCK_ID).call().await?; let base_class_hash = world.base().block_id(BLOCK_ID).call().await?; - let executor_class_hash = world - .provider() - .get_class_hash_at(BLOCK_ID, FieldElement::from(executor_address)) - .await - .map_err(|err| match err { - ProviderError::StarknetError(StarknetError::ContractNotFound) => { - ManifestError::ExecutorNotFound - } - err => err.into(), - })?; + let (resource_metadata_class_hash, resource_metadata_address) = world + .model(&FieldElement::from_hex_be(RESOURCE_METADATA_MODEL_NAME).unwrap()) + .block_id(BLOCK_ID) + .call() + .await?; + + let resource_metadata_address = + if resource_metadata_address == ContractAddress(FieldElement::ZERO) { + None + } else { + Some(resource_metadata_address.into()) + }; let (models, contracts) = get_remote_models_and_contracts(world_address, &world.provider()).await?; @@ -200,10 +201,10 @@ impl Manifest { address: Some(world_address), ..Default::default() }, - executor: Contract { - name: EXECUTOR_CONTRACT_NAME.into(), - address: Some(executor_address.into()), - class_hash: executor_class_hash, + resource_metadata: Contract { + name: RESOURCE_METADATA_CONTRACT_NAME.into(), + class_hash: resource_metadata_class_hash.into(), + address: resource_metadata_address, ..Default::default() }, base: Class { @@ -381,24 +382,27 @@ fn parse_contracts_events( fn parse_models_events(events: Vec) -> Vec { let mut models: HashMap = HashMap::with_capacity(events.len()); - for event in events { - let mut data = event.data.into_iter(); - - let model_name = data.next().expect("name is missing from event"); - let model_name = parse_cairo_short_string(&model_name).unwrap(); + for e in events { + let model_event = if let WorldEvent::ModelRegistered(m) = + e.try_into().expect("ModelRegistered event is expected to be parseable") + { + m + } else { + panic!("ModelRegistered expected"); + }; - let class_hash = data.next().expect("class hash is missing from event"); - let prev_class_hash = data.next().expect("prev class hash is missing from event"); + let model_name = parse_cairo_short_string(&model_event.name).unwrap(); if let Some(current_class_hash) = models.get_mut(&model_name) { - if current_class_hash == &prev_class_hash { - *current_class_hash = class_hash; + if current_class_hash == &model_event.prev_class_hash.into() { + *current_class_hash = model_event.class_hash.into(); } } else { - models.insert(model_name, class_hash); + models.insert(model_name, model_event.class_hash.into()); } } + // TODO: include address of the model in the manifest. models .into_iter() .map(|(name, class_hash)| Model { name, class_hash, ..Default::default() }) diff --git a/crates/dojo-world/src/manifest_test.rs b/crates/dojo-world/src/manifest_test.rs index 1ad5b0d696..51452ea898 100644 --- a/crates/dojo-world/src/manifest_test.rs +++ b/crates/dojo-world/src/manifest_test.rs @@ -6,7 +6,7 @@ use dojo_test_utils::sequencer::{ use serde_json::json; use starknet::accounts::ConnectedAccount; use starknet::core::types::{EmittedEvent, FieldElement}; -use starknet::macros::{felt, short_string}; +use starknet::macros::{felt, selector, short_string}; use starknet::providers::jsonrpc::{JsonRpcClient, JsonRpcMethod}; use super::{parse_contracts_events, Contract, Manifest, Model}; @@ -47,26 +47,46 @@ fn parse_registered_model_events() { Model { name: "Model2".into(), class_hash: felt!("0x6666"), ..Default::default() }, ]; + let selector = selector!("ModelRegistered"); + let events = vec![ EmittedEvent { - data: vec![short_string!("Model1"), felt!("0x5555"), felt!("0xbeef")], - keys: vec![], + data: vec![ + short_string!("Model1"), + felt!("0x5555"), + felt!("0xbeef"), + felt!("0xa1"), + felt!("0"), + ], + keys: vec![selector], block_hash: Default::default(), from_address: Default::default(), block_number: Default::default(), transaction_hash: Default::default(), }, EmittedEvent { - data: vec![short_string!("Model1"), felt!("0xbeef"), felt!("0")], - keys: vec![], + data: vec![ + short_string!("Model1"), + felt!("0xbeef"), + felt!("0"), + felt!("0xa1"), + felt!("0xa1"), + ], + keys: vec![selector], block_hash: Default::default(), from_address: Default::default(), block_number: Default::default(), transaction_hash: Default::default(), }, EmittedEvent { - data: vec![short_string!("Model2"), felt!("0x6666"), felt!("0")], - keys: vec![], + data: vec![ + short_string!("Model2"), + felt!("0x6666"), + felt!("0"), + felt!("0xa3"), + felt!("0"), + ], + keys: vec![selector], block_hash: Default::default(), from_address: Default::default(), block_number: Default::default(), @@ -236,7 +256,7 @@ async fn fetch_remote_manifest() { Utf8PathBuf::from_path_buf("../../examples/spawn-and-move/target/dev".into()).unwrap(); let manifest_path = artifacts_path.join("manifest.json"); - let (world_address, _) = deploy_world(&sequencer, artifacts_path).await; + let world_address = deploy_world(&sequencer, artifacts_path).await; let local_manifest = Manifest::load_from_path(manifest_path).unwrap(); let remote_manifest = Manifest::load_from_remote(provider, world_address).await.unwrap(); diff --git a/crates/dojo-world/src/migration/mod.rs b/crates/dojo-world/src/migration/mod.rs index 5b3d256fec..38f4fb6953 100644 --- a/crates/dojo-world/src/migration/mod.rs +++ b/crates/dojo-world/src/migration/mod.rs @@ -145,7 +145,10 @@ pub trait Deployable: Declarable + Sync { let declare = match self.declare(account, txn_config).await { Ok(res) => Some(res), Err(MigrationError::ClassAlreadyDeclared) => None, - Err(e) => return Err(e), + Err(e) => { + println!("{:?}", e); + return Err(e); + } }; let base_class_hash = account diff --git a/crates/dojo-world/src/migration/strategy.rs b/crates/dojo-world/src/migration/strategy.rs index cde6af17ee..a0cced1b05 100644 --- a/crates/dojo-world/src/migration/strategy.rs +++ b/crates/dojo-world/src/migration/strategy.rs @@ -15,7 +15,6 @@ use super::{DeployOutput, MigrationType, RegisterOutput}; #[derive(Debug)] pub struct MigrationOutput { pub world: Option, - pub executor: Option, pub contracts: Vec, pub models: Option, } @@ -24,8 +23,8 @@ pub struct MigrationOutput { pub struct MigrationStrategy { pub world_address: Option, pub world: Option, - pub executor: Option, pub base: Option, + pub resource_metadata: Option, pub contracts: Vec, pub models: Vec, } @@ -55,13 +54,6 @@ impl MigrationStrategy { } } - if let Some(item) = &self.executor { - match item.migration_type() { - MigrationType::New => new += 1, - MigrationType::Update => update += 1, - } - } - self.contracts.iter().for_each(|item| match item.migration_type() { MigrationType::New => new += 1, MigrationType::Update => update += 1, @@ -109,18 +101,13 @@ where // If the world contract needs to be migrated, then all contracts need to be migrated // else we need to evaluate which contracts need to be migrated. let mut world = evaluate_contract_to_migrate(&diff.world, &artifact_paths, false)?; - let mut executor = - evaluate_contract_to_migrate(&diff.executor, &artifact_paths, world.is_some())?; let base = evaluate_class_to_migrate(&diff.base, &artifact_paths, world.is_some())?; + let resource_metadata = + evaluate_class_to_migrate(&diff.resource_metadata, &artifact_paths, world.is_some())?; let contracts = evaluate_contracts_to_migrate(&diff.contracts, &artifact_paths, world.is_some())?; let models = evaluate_models_to_migrate(&diff.models, &artifact_paths, world.is_some())?; - if let Some(executor) = &mut executor { - executor.contract_address = - get_contract_address(FieldElement::ZERO, diff.executor.local, &[], FieldElement::ZERO); - } - // If world needs to be migrated, then we expect the `seed` to be provided. if let Some(world) = &mut world { let salt = @@ -130,12 +117,12 @@ where world.contract_address = get_contract_address( salt, diff.world.local, - &[executor.as_ref().unwrap().contract_address, base.as_ref().unwrap().diff.local], + &[base.as_ref().unwrap().diff.local], FieldElement::ZERO, ); } - Ok(MigrationStrategy { world_address, world, executor, base, contracts, models }) + Ok(MigrationStrategy { world_address, world, resource_metadata, base, contracts, models }) } fn evaluate_models_to_migrate( diff --git a/crates/dojo-world/src/migration/world.rs b/crates/dojo-world/src/migration/world.rs index a7d8a9cb25..9b3613590d 100644 --- a/crates/dojo-world/src/migration/world.rs +++ b/crates/dojo-world/src/migration/world.rs @@ -5,7 +5,9 @@ use convert_case::{Case, Casing}; use super::class::ClassDiff; use super::contract::ContractDiff; use super::StateDiff; -use crate::manifest::{Manifest, BASE_CONTRACT_NAME, EXECUTOR_CONTRACT_NAME, WORLD_CONTRACT_NAME}; +use crate::manifest::{ + Manifest, BASE_CONTRACT_NAME, RESOURCE_METADATA_CONTRACT_NAME, WORLD_CONTRACT_NAME, +}; #[cfg(test)] #[path = "world_test.rs"] @@ -15,8 +17,8 @@ mod tests; #[derive(Debug, Clone)] pub struct WorldDiff { pub world: ContractDiff, - pub executor: ContractDiff, pub base: ClassDiff, + pub resource_metadata: ClassDiff, pub contracts: Vec, pub models: Vec, } @@ -62,25 +64,25 @@ impl WorldDiff { }) .collect::>(); - let executor = ContractDiff { - name: EXECUTOR_CONTRACT_NAME.into(), - local: local.executor.class_hash, - remote: remote.as_ref().map(|m| m.executor.class_hash), - }; - let base = ClassDiff { name: BASE_CONTRACT_NAME.into(), local: local.base.class_hash, remote: remote.as_ref().map(|m| m.base.class_hash), }; + let resource_metadata = ClassDiff { + name: RESOURCE_METADATA_CONTRACT_NAME.into(), + local: local.resource_metadata.class_hash, + remote: remote.as_ref().map(|m| m.resource_metadata.class_hash), + }; + let world = ContractDiff { name: WORLD_CONTRACT_NAME.into(), local: local.world.class_hash, remote: remote.map(|m| m.world.class_hash), }; - WorldDiff { world, executor, base, contracts, models } + WorldDiff { world, base, resource_metadata, contracts, models } } pub fn count_diffs(&self) -> usize { @@ -90,10 +92,6 @@ impl WorldDiff { count += 1; } - if !self.executor.is_same() { - count += 1; - } - count += self.models.iter().filter(|s| !s.is_same()).count(); count += self.contracts.iter().filter(|s| !s.is_same()).count(); count @@ -103,7 +101,6 @@ impl WorldDiff { impl Display for WorldDiff { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "{}", self.world)?; - writeln!(f, "{}", self.executor)?; for model in &self.models { writeln!(f, "{model}")?; diff --git a/crates/dojo-world/src/migration/world_test.rs b/crates/dojo-world/src/migration/world_test.rs index 698500f401..31f96b1642 100644 --- a/crates/dojo-world/src/migration/world_test.rs +++ b/crates/dojo-world/src/migration/world_test.rs @@ -12,13 +12,6 @@ fn no_diff_when_local_and_remote_are_equal() { ..Default::default() }; - let executor_contract = Contract { - address: Some(88_u32.into()), - class_hash: 99_u32.into(), - name: EXECUTOR_CONTRACT_NAME.into(), - ..Default::default() - }; - let models = vec![Model { members: vec![], name: "dojo_mock::models::model".into(), @@ -33,12 +26,7 @@ fn no_diff_when_local_and_remote_are_equal() { ..Default::default() }]; - let local = Manifest { - models, - world: world_contract, - executor: executor_contract, - ..Default::default() - }; + let local = Manifest { models, world: world_contract, ..Default::default() }; let mut remote = local.clone(); remote.models = remote_models; @@ -56,12 +44,6 @@ fn diff_when_local_and_remote_are_different() { ..Default::default() }; - let executor_contract = Contract { - class_hash: 99_u32.into(), - name: EXECUTOR_CONTRACT_NAME.into(), - ..Default::default() - }; - let models = vec![ Model { members: vec![], @@ -107,23 +89,17 @@ fn diff_when_local_and_remote_are_different() { }, ]; - let local = Manifest { - models, - contracts, - world: world_contract, - executor: executor_contract, - ..Default::default() - }; + let local = Manifest { models, contracts, world: world_contract, ..Default::default() }; let mut remote = local.clone(); remote.models = remote_models; remote.world.class_hash = 44_u32.into(); - remote.executor.class_hash = 55_u32.into(); + remote.models[1].class_hash = 33_u32.into(); remote.contracts[0].class_hash = felt!("0x1112"); let diff = WorldDiff::compute(local, Some(remote)); - assert_eq!(diff.count_diffs(), 4); + assert_eq!(diff.count_diffs(), 3); assert!(diff.models.iter().any(|m| m.name == "dojo_mock::models::model_2")); assert!(diff.contracts.iter().any(|c| c.name == "dojo_mock::contracts::my_contract")); } diff --git a/crates/torii/client/src/client/storage.rs b/crates/torii/client/src/client/storage.rs index a647f6cf64..48541a2314 100644 --- a/crates/torii/client/src/client/storage.rs +++ b/crates/torii/client/src/client/storage.rs @@ -183,6 +183,7 @@ mod tests { dojo_types::schema::ModelMetadata { name: "Position".into(), class_hash: felt!("1"), + contract_address: felt!("2"), packed_size: 4, unpacked_size: 4, layout: vec![], diff --git a/crates/torii/client/src/client/subscription.rs b/crates/torii/client/src/client/subscription.rs index fccb86ce50..ac21707104 100644 --- a/crates/torii/client/src/client/subscription.rs +++ b/crates/torii/client/src/client/subscription.rs @@ -257,6 +257,7 @@ mod tests { dojo_types::schema::ModelMetadata { name: "Position".into(), class_hash: felt!("1"), + contract_address: felt!("2"), packed_size: 1, unpacked_size: 2, layout: vec![], diff --git a/crates/torii/core/src/model.rs b/crates/torii/core/src/model.rs index 013163b10d..ba4d42d81c 100644 --- a/crates/torii/core/src/model.rs +++ b/crates/torii/core/src/model.rs @@ -17,6 +17,8 @@ pub struct ModelSQLReader { name: String, /// The class hash of the model class_hash: FieldElement, + /// The contract address of the model + contract_address: FieldElement, pool: Pool, packed_size: FieldElement, unpacked_size: FieldElement, @@ -25,14 +27,16 @@ pub struct ModelSQLReader { impl ModelSQLReader { pub async fn new(name: &str, pool: Pool) -> Result { - let (name, class_hash, packed_size, unpacked_size, layout): ( + let (name, class_hash, contract_address, packed_size, unpacked_size, layout): ( + String, String, String, u32, u32, String, ) = sqlx::query_as( - "SELECT name, class_hash, packed_size, unpacked_size, layout FROM models WHERE id = ?", + "SELECT name, class_hash, contract_address, packed_size, unpacked_size, layout FROM \ + models WHERE id = ?", ) .bind(name) .fetch_one(&pool) @@ -40,13 +44,15 @@ impl ModelSQLReader { let class_hash = FieldElement::from_hex_be(&class_hash).map_err(error::ParseError::FromStr)?; + let contract_address = + FieldElement::from_hex_be(&contract_address).map_err(error::ParseError::FromStr)?; let packed_size = FieldElement::from(packed_size); let unpacked_size = FieldElement::from(unpacked_size); let layout = hex::decode(layout).unwrap(); let layout = layout.iter().map(|e| FieldElement::from(*e)).collect(); - Ok(Self { name, class_hash, pool, packed_size, unpacked_size, layout }) + Ok(Self { name, class_hash, contract_address, pool, packed_size, unpacked_size, layout }) } } @@ -57,6 +63,10 @@ impl ModelReader for ModelSQLReader { self.class_hash } + fn contract_address(&self) -> FieldElement { + self.contract_address + } + async fn schema(&self) -> Result { let model_members: Vec = sqlx::query_as( "SELECT id, model_idx, member_idx, name, type, type_enum, enum_options, key FROM \ @@ -262,7 +272,7 @@ pub fn map_row_to_ty(path: &str, struct_ty: &mut Struct, row: &SqliteRow) -> Res } Primitive::ClassHash(_) => { let value = row.try_get::(&column_name)?; - primitive.set_class_hash(Some( + primitive.set_contract_address(Some( FieldElement::from_str(&value).map_err(ParseError::FromStr)?, ))?; } diff --git a/crates/torii/core/src/processors/register_model.rs b/crates/torii/core/src/processors/register_model.rs index 1771605f6c..c6f01fce6f 100644 --- a/crates/torii/core/src/processors/register_model.rs +++ b/crates/torii/core/src/processors/register_model.rs @@ -5,7 +5,7 @@ use dojo_world::contracts::world::WorldContractReader; use starknet::core::types::{BlockWithTxs, Event, InvokeTransactionReceipt}; use starknet::core::utils::parse_cairo_short_string; use starknet::providers::Provider; -use tracing::info; +use tracing::{debug, info}; use super::EventProcessor; use crate::sql::Sql; @@ -52,9 +52,23 @@ where let unpacked_size: u32 = model.unpacked_size().await?.try_into()?; let packed_size: u32 = model.packed_size().await?.try_into()?; - info!("Registered model: {}", name); + let class_hash = event.data[1]; + let contract_address = event.data[3]; - db.register_model(schema, layout, event.data[1], packed_size, unpacked_size).await?; + info!(name, "Registered model"); + debug!( + name, + ?schema, + ?layout, + ?class_hash, + ?contract_address, + packed_size, + unpacked_size, + "Registered model content" + ); + + db.register_model(schema, layout, class_hash, contract_address, packed_size, unpacked_size) + .await?; Ok(()) } diff --git a/crates/torii/core/src/processors/store_set_record.rs b/crates/torii/core/src/processors/store_set_record.rs index 1da641a5be..dc650f0459 100644 --- a/crates/torii/core/src/processors/store_set_record.rs +++ b/crates/torii/core/src/processors/store_set_record.rs @@ -53,8 +53,10 @@ where let keys_end: usize = keys_start + usize::from(u8::try_from(event.data[NUM_KEYS_INDEX])?); let keys = event.data[keys_start..keys_end].to_vec(); - let values_start = keys_end + 2; - let values_end: usize = values_start + usize::from(u8::try_from(event.data[keys_end + 1])?); + // keys_end is already the length of the values array. + + let values_start = keys_end + 1; + let values_end: usize = values_start + usize::from(u8::try_from(event.data[keys_end])?); let values = event.data[values_start..values_end].to_vec(); let mut keys_and_unpacked = [keys, values].concat(); diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index 946f0994ce..5bd94ce514 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -79,6 +79,7 @@ impl Sql { model: Ty, layout: Vec, class_hash: FieldElement, + contract_address: FieldElement, packed_size: u32, unpacked_size: u32, ) -> Result<()> { @@ -87,15 +88,17 @@ impl Sql { .map(|x| >::try_into(*x).unwrap()) .collect::>(); - let insert_models = "INSERT INTO models (id, name, class_hash, layout, packed_size, \ - unpacked_size) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE \ - SET class_hash=EXCLUDED.class_hash, layout=EXCLUDED.layout, \ - packed_size=EXCLUDED.packed_size, \ - unpacked_size=EXCLUDED.unpacked_size RETURNING *"; + let insert_models = + "INSERT INTO models (id, name, class_hash, contract_address, layout, packed_size, \ + unpacked_size) VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET \ + contract_address=EXCLUDED.contract_address, class_hash=EXCLUDED.class_hash, \ + layout=EXCLUDED.layout, packed_size=EXCLUDED.packed_size, \ + unpacked_size=EXCLUDED.unpacked_size RETURNING *"; let model_registered: ModelRegistered = sqlx::query_as(insert_models) .bind(model.name()) .bind(model.name()) .bind(format!("{class_hash:#x}")) + .bind(format!("{contract_address:#x}")) .bind(hex::encode(&layout_blob)) .bind(packed_size) .bind(unpacked_size) diff --git a/crates/torii/core/src/types.rs b/crates/torii/core/src/types.rs index cfca4769bb..af4328b90e 100644 --- a/crates/torii/core/src/types.rs +++ b/crates/torii/core/src/types.rs @@ -44,6 +44,7 @@ pub struct Model { pub id: String, pub name: String, pub class_hash: String, + pub contract_address: String, pub transaction_hash: String, pub created_at: DateTime, } diff --git a/crates/torii/graphql/src/mapping.rs b/crates/torii/graphql/src/mapping.rs index 9e139e3b48..2aefccbd91 100644 --- a/crates/torii/graphql/src/mapping.rs +++ b/crates/torii/graphql/src/mapping.rs @@ -38,6 +38,10 @@ lazy_static! { Name::new("classHash"), TypeData::Simple(TypeRef::named(Primitive::Felt252(None).to_string())), ), + ( + Name::new("contractAddress"), + TypeData::Simple(TypeRef::named(Primitive::Felt252(None).to_string())), + ), ( Name::new("transactionHash"), TypeData::Simple(TypeRef::named(Primitive::Felt252(None).to_string())), diff --git a/crates/torii/graphql/src/object/model.rs b/crates/torii/graphql/src/object/model.rs index 42c95a2127..f974810839 100644 --- a/crates/torii/graphql/src/object/model.rs +++ b/crates/torii/graphql/src/object/model.rs @@ -106,6 +106,7 @@ impl ModelObject { (Name::new("id"), Value::from(model.id)), (Name::new("name"), Value::from(model.name)), (Name::new("classHash"), Value::from(model.class_hash)), + (Name::new("contractAddress"), Value::from(model.contract_address)), (Name::new("transactionHash"), Value::from(model.transaction_hash)), ( Name::new("createdAt"), diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index fda7c9e444..232436d16a 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -246,6 +246,7 @@ pub async fn model_fixtures(db: &mut Sql) { }), vec![], FieldElement::ONE, + FieldElement::TWO, 0, 0, ) diff --git a/crates/torii/graphql/src/tests/subscription_test.rs b/crates/torii/graphql/src/tests/subscription_test.rs index 879d209dec..1f54c84f3a 100644 --- a/crates/torii/graphql/src/tests/subscription_test.rs +++ b/crates/torii/graphql/src/tests/subscription_test.rs @@ -244,6 +244,7 @@ mod tests { let model_name = "Subrecord".to_string(); let model_id = model_name.clone(); let class_hash = FieldElement::TWO; + let contract_address = FieldElement::THREE; let expected_value: async_graphql::Value = value!({ "modelRegistered": { "id": model_id, "name":model_name } }); @@ -261,7 +262,7 @@ mod tests { ty: Ty::Primitive(Primitive::U32(None)), }], }); - db.register_model(model, vec![], class_hash, 0, 0).await.unwrap(); + db.register_model(model, vec![], class_hash, contract_address, 0, 0).await.unwrap(); // 3. fn publish() is called from state.set_entity() @@ -293,6 +294,7 @@ mod tests { let model_name = "Subrecord".to_string(); let model_id = model_name.clone(); let class_hash = FieldElement::TWO; + let contract_address = FieldElement::THREE; let expected_value: async_graphql::Value = value!({ "modelRegistered": { "id": model_id, "name":model_name } }); @@ -310,7 +312,7 @@ mod tests { ty: Ty::Primitive(Primitive::U8(None)), }], }); - db.register_model(model, vec![], class_hash, 0, 0).await.unwrap(); + db.register_model(model, vec![], class_hash, contract_address, 0, 0).await.unwrap(); // 3. fn publish() is called from state.set_entity() tx.send(()).await.unwrap(); diff --git a/crates/torii/grpc/build.rs b/crates/torii/grpc/build.rs index 34f86df1b0..b12cb0891b 100644 --- a/crates/torii/grpc/build.rs +++ b/crates/torii/grpc/build.rs @@ -24,5 +24,8 @@ fn main() -> Result<(), Box> { .file_descriptor_set_path(out_dir.join("world_descriptor.bin")) .compile(&["proto/world.proto"], &["proto"])?; } + + println!("cargo:rerun-if-changed=proto"); + Ok(()) } diff --git a/crates/torii/grpc/proto/types.proto b/crates/torii/grpc/proto/types.proto index abb8495b9c..78f553fecb 100644 --- a/crates/torii/grpc/proto/types.proto +++ b/crates/torii/grpc/proto/types.proto @@ -29,6 +29,8 @@ message ModelMetadata { bytes layout = 5; // The schema of the component serialized in bytes (for simplicity sake) bytes schema = 6; + // hex-encoded contract address of the component + string contract_address = 7; } message Model { diff --git a/crates/torii/grpc/src/server/mod.rs b/crates/torii/grpc/src/server/mod.rs index a6c3625a28..857b04b5a3 100644 --- a/crates/torii/grpc/src/server/mod.rs +++ b/crates/torii/grpc/src/server/mod.rs @@ -88,8 +88,9 @@ impl DojoWorld { .fetch_one(&self.pool) .await?; - let models: Vec<(String, String, u32, u32, String)> = sqlx::query_as( - "SELECT name, class_hash, packed_size, unpacked_size, layout FROM models", + let models: Vec<(String, String, String, u32, u32, String)> = sqlx::query_as( + "SELECT name, class_hash, contract_address, packed_size, unpacked_size, layout FROM \ + models", ) .fetch_all(&self.pool) .await?; @@ -100,9 +101,10 @@ impl DojoWorld { models_metadata.push(proto::types::ModelMetadata { name: model.0, class_hash: model.1, - packed_size: model.2, - unpacked_size: model.3, - layout: hex::decode(&model.4).unwrap(), + contract_address: model.2, + packed_size: model.3, + unpacked_size: model.4, + layout: hex::decode(&model.5).unwrap(), schema: serde_json::to_vec(&schema).unwrap(), }); } @@ -312,14 +314,16 @@ impl DojoWorld { } pub async fn model_metadata(&self, model: &str) -> Result { - let (name, class_hash, packed_size, unpacked_size, layout): ( + let (name, class_hash, contract_address, packed_size, unpacked_size, layout): ( + String, String, String, u32, u32, String, ) = sqlx::query_as( - "SELECT name, class_hash, packed_size, unpacked_size, layout FROM models WHERE id = ?", + "SELECT name, class_hash, contract_address, packed_size, unpacked_size, layout FROM \ + models WHERE id = ?", ) .bind(model) .fetch_one(&self.pool) @@ -332,6 +336,7 @@ impl DojoWorld { name, layout, class_hash, + contract_address, packed_size, unpacked_size, schema: serde_json::to_vec(&schema).unwrap(), diff --git a/crates/torii/grpc/src/types/mod.rs b/crates/torii/grpc/src/types/mod.rs index bc18d7f5cf..ca39ed5510 100644 --- a/crates/torii/grpc/src/types/mod.rs +++ b/crates/torii/grpc/src/types/mod.rs @@ -125,6 +125,7 @@ impl TryFrom for dojo_types::schema::ModelMetadata packed_size: value.packed_size, unpacked_size: value.unpacked_size, class_hash: FieldElement::from_str(&value.class_hash)?, + contract_address: FieldElement::from_str(&value.contract_address)?, }) } } @@ -142,8 +143,6 @@ impl TryFrom for dojo_types::WorldMetadata { models, world_address: FieldElement::from_str(&value.world_address)?, world_class_hash: FieldElement::from_str(&value.world_class_hash)?, - executor_address: FieldElement::from_str(&value.executor_address)?, - executor_class_hash: FieldElement::from_str(&value.executor_class_hash)?, }) } } diff --git a/crates/torii/libp2p/src/client/mod.rs b/crates/torii/libp2p/src/client/mod.rs index a947117210..9d26458917 100644 --- a/crates/torii/libp2p/src/client/mod.rs +++ b/crates/torii/libp2p/src/client/mod.rs @@ -126,7 +126,9 @@ impl RelayClient { .expect("Failed to create WebRTC transport") .with_behaviour(|key| { let gossipsub_config: gossipsub::Config = gossipsub::ConfigBuilder::default() - .heartbeat_interval(std::time::Duration::from_secs(10)) + .heartbeat_interval(Duration::from_secs( + constants::GOSSIPSUB_HEARTBEAT_INTERVAL_SECS, + )) .build() .expect("Gossipsup config is invalid"); @@ -143,7 +145,11 @@ impl RelayClient { ping: ping::Behaviour::new(ping::Config::default()), } })? - .with_swarm_config(|cfg| cfg.with_idle_connection_timeout(Duration::from_secs(60))) + .with_swarm_config(|cfg| { + cfg.with_idle_connection_timeout(Duration::from_secs( + constants::IDLE_CONNECTION_TIMEOUT_SECS, + )) + }) .build(); info!(target: "torii::relay::client", addr = %relay_addr, "Dialing relay"); diff --git a/crates/torii/migrations/20230316154230_setup.sql b/crates/torii/migrations/20230316154230_setup.sql index 254c351342..4f9edb7065 100644 --- a/crates/torii/migrations/20230316154230_setup.sql +++ b/crates/torii/migrations/20230316154230_setup.sql @@ -83,4 +83,4 @@ CREATE TABLE events ( created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); -CREATE INDEX idx_events_keys ON events (keys); \ No newline at end of file +CREATE INDEX idx_events_keys ON events (keys); diff --git a/crates/torii/migrations/20231030154053_remove_syscalls_add_txn.sql b/crates/torii/migrations/20231030154053_remove_syscalls_add_txn.sql index 8ff7d6a993..026139fb02 100644 --- a/crates/torii/migrations/20231030154053_remove_syscalls_add_txn.sql +++ b/crates/torii/migrations/20231030154053_remove_syscalls_add_txn.sql @@ -22,4 +22,4 @@ CREATE TABLE transaction_receipts ( execution_result TEXT NOT NULL, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE (transaction_hash) -); \ No newline at end of file +); diff --git a/crates/torii/migrations/20240214194914_model_contract_address.sql b/crates/torii/migrations/20240214194914_model_contract_address.sql new file mode 100644 index 0000000000..8762fbcf0e --- /dev/null +++ b/crates/torii/migrations/20240214194914_model_contract_address.sql @@ -0,0 +1,3 @@ +-- Models have now a contract address. +ALTER TABLE models +ADD COLUMN contract_address TEXT DEFAULT '0' NOT NULL; diff --git a/crates/torii/types-test/Scarb.lock b/crates/torii/types-test/Scarb.lock index 0081e547e0..69570413ff 100644 --- a/crates/torii/types-test/Scarb.lock +++ b/crates/torii/types-test/Scarb.lock @@ -15,7 +15,7 @@ source = "git+https://github.com/dojoengine/dojo?tag=v0.3.11#1e651b5d4d3b79b14a7 [[package]] name = "types_test" -version = "0.5.0" +version = "0.5.1" dependencies = [ "dojo", ] diff --git a/crates/torii/types-test/src/contracts.cairo b/crates/torii/types-test/src/contracts.cairo index ed32472ede..821e8f957a 100644 --- a/crates/torii/types-test/src/contracts.cairo +++ b/crates/torii/types-test/src/contracts.cairo @@ -99,7 +99,7 @@ mod records { }, random_u8, random_u128, - composite_u256 + composite_u256, }, RecordSibling { record_id, random_u8 }, Subrecord { diff --git a/crates/torii/types-test/src/models.cairo b/crates/torii/types-test/src/models.cairo index ad019425ad..77e7c69f6b 100644 --- a/crates/torii/types-test/src/models.cairo +++ b/crates/torii/types-test/src/models.cairo @@ -1,7 +1,7 @@ use array::ArrayTrait; use starknet::{ContractAddress, ClassHash}; -#[derive(Model, Copy, Drop, Serde)] +#[derive(Model, Drop, Serde)] struct Record { #[key] record_id: u32, diff --git a/examples/spawn-and-move/Scarb.lock b/examples/spawn-and-move/Scarb.lock index 0a3638b88d..af4e4b139f 100644 --- a/examples/spawn-and-move/Scarb.lock +++ b/examples/spawn-and-move/Scarb.lock @@ -10,7 +10,7 @@ dependencies = [ [[package]] name = "dojo_examples" -version = "0.5.0" +version = "0.5.1" dependencies = [ "dojo", ]