diff --git a/Cargo.lock b/Cargo.lock index 51a284c926..221db839c3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7869,6 +7869,7 @@ dependencies = [ "dojo-world", "futures", "futures-util", + "http", "js-sys", "parking_lot 0.12.1", "prost 0.11.9", diff --git a/crates/dojo-core/src/test_utils.cairo b/crates/dojo-core/src/test_utils.cairo index d4abfa7f75..e22d12c671 100644 --- a/crates/dojo-core/src/test_utils.cairo +++ b/crates/dojo-core/src/test_utils.cairo @@ -21,9 +21,7 @@ use dojo::world::{world, IWorldDispatcher, IWorldDispatcherTrait}; /// # Returns /// * address of contract deployed fn deploy_contract(class_hash: felt252, calldata: Span) -> ContractAddress { - let (contract, _) = starknet::deploy_syscall( - class_hash.try_into().unwrap(), 0, calldata, false - ) + let (contract, _) = starknet::deploy_syscall(class_hash.try_into().unwrap(), 0, calldata, false) .unwrap(); contract } @@ -50,7 +48,10 @@ fn spawn_test_world(models: Array) -> IWorldDispatcher { .unwrap(); // deploy world let (world_address, _) = deploy_syscall( - world::TEST_CLASS_HASH.try_into().unwrap(), 0, array![executor_address.into(), dojo::base::base::TEST_CLASS_HASH].span(), false + world::TEST_CLASS_HASH.try_into().unwrap(), + 0, + array![executor_address.into(), dojo::base::base::TEST_CLASS_HASH, 1, 'test_uri'].span(), + false ) .unwrap(); let world = IWorldDispatcher { contract_address: world_address }; diff --git a/crates/dojo-core/src/world.cairo b/crates/dojo-core/src/world.cairo index d6bc989f15..4ccda20dee 100644 --- a/crates/dojo-core/src/world.cairo +++ b/crates/dojo-core/src/world.cairo @@ -4,18 +4,15 @@ use option::OptionTrait; #[starknet::interface] trait IWorld { + fn metadata_uri(self: @T) -> Span; + fn set_metadata_uri(ref self: T, uri: Span); fn model(self: @T, name: felt252) -> ClassHash; fn register_model(ref self: T, class_hash: ClassHash); fn deploy_contract(self: @T, salt: felt252, class_hash: ClassHash) -> ContractAddress; 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, offset: u8, length: usize, layout: Span ) -> Span; fn set_entity( ref self: T, @@ -26,7 +23,12 @@ trait IWorld { layout: Span ); fn entities( - self: @T, model: felt252, index: Option, values: Span, values_length: usize, values_layout: Span + self: @T, + model: felt252, + index: Option, + values: Span, + values_length: usize, + values_layout: Span ) -> (Span, Span>); fn set_executor(ref self: T, contract_address: ContractAddress); fn executor(self: @T) -> ContractAddress; @@ -51,7 +53,8 @@ mod world { use starknet::{ get_caller_address, get_contract_address, get_tx_info, contract_address::ContractAddressIntoFelt252, ClassHash, Zeroable, ContractAddress, - syscalls::{deploy_syscall, emit_event_syscall}, SyscallResult, SyscallResultTrait, SyscallResultTraitImpl + syscalls::{deploy_syscall, emit_event_syscall}, SyscallResult, SyscallResultTrait, + SyscallResultTraitImpl }; use dojo::database; @@ -78,7 +81,8 @@ mod world { #[derive(Drop, starknet::Event)] struct WorldSpawned { address: ContractAddress, - caller: ContractAddress + caller: ContractAddress, + metadata_uri: Span } #[derive(Drop, starknet::Event)] @@ -106,28 +110,27 @@ mod world { executor_dispatcher: IExecutorDispatcher, contract_base: ClassHash, nonce: usize, + metadata_uri: LegacyMap::, models: 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, + executor: ContractAddress, + contract_base: ClassHash, + metadata_uri: Span + ) { + let caller = get_caller_address(); self.executor_dispatcher.write(IExecutorDispatcher { contract_address: executor }); self.contract_base.write(contract_base); - - self - .owners - .write( - (WORLD, starknet::get_tx_info().unbox().account_contract_address), bool::True(()) - ); + self.owners.write((WORLD, caller), true); + self.set_metadata_uri(metadata_uri); EventEmitter::emit( - ref self, - WorldSpawned { - address: get_contract_address(), - caller: get_tx_info().unbox().account_contract_address - } + ref self, WorldSpawned { address: get_contract_address(), caller, metadata_uri } ); } @@ -151,6 +154,54 @@ mod world { #[external(v0)] impl World of IWorld { + /// Returns the metadata URI of the world. + /// + /// # Returns + /// + /// * `Span` - The metadata URI of the world. + fn metadata_uri(self: @ContractState) -> Span { + let mut uri = array![]; + + // We add one here since we start i at 1; + let len = self.metadata_uri.read(0) + 1; + + let mut i: usize = 1; + loop { + if len == i.into() { + break; + } + + uri.append(self.metadata_uri.read(i)); + i += 1; + }; + + uri.span() + } + + /// Sets the metadata URI of the world. + /// + /// # Arguments + /// + /// * `uri` - The new metadata URI to be set. + fn set_metadata_uri(ref self: ContractState, mut uri: Span) { + assert(self.is_owner(get_caller_address(), WORLD), 'not owner'); + + self.metadata_uri.write(0, uri.len().into()); + + let mut i: usize = 1; + loop { + match uri.pop_front() { + Option::Some(item) => { + self.metadata_uri.write(i, *item); + i += 1; + }, + Option::None(_) => { + break; + } + }; + }; + } + /// Checks if the provided account is an owner of the target. /// /// # Arguments @@ -216,8 +267,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), 'not owner or writer' ); self.writers.write((model, system), bool::True(())); } @@ -286,8 +336,13 @@ mod world { /// # Returns /// /// * `ClassHash` - The class hash of the model. - fn deploy_contract(self: @ContractState, salt: felt252, class_hash: ClassHash) -> ContractAddress { - let (contract_address, _) = deploy_syscall(self.contract_base.read(), salt, array![].span(), false).unwrap_syscall(); + fn deploy_contract( + self: @ContractState, salt: felt252, class_hash: ClassHash + ) -> ContractAddress { + let (contract_address, _) = deploy_syscall( + self.contract_base.read(), salt, array![].span(), false + ) + .unwrap_syscall(); let upgradable_dispatcher = IUpgradeableDispatcher { contract_address }; upgradable_dispatcher.upgrade(class_hash); contract_address @@ -399,7 +454,12 @@ mod world { /// * `Span` - The entity IDs. /// * `Span>` - The entities. fn entities( - self: @ContractState, model: felt252, index: Option, values: Span, values_length: usize, values_layout: Span + self: @ContractState, + model: felt252, + index: Option, + values: Span, + values_length: usize, + values_layout: Span ) -> (Span, Span>) { let class_hash = self.models.read(model); diff --git a/crates/dojo-core/src/world_test.cairo b/crates/dojo-core/src/world_test.cairo index 15271cc838..246e316a9f 100644 --- a/crates/dojo-core/src/world_test.cairo +++ b/crates/dojo-core/src/world_test.cairo @@ -170,6 +170,35 @@ fn deploy_world() -> IWorldDispatcher { spawn_test_world(array![]) } +#[test] +#[available_gas(60000000)] +fn test_metadata_uri() { + // Deploy world contract + let world = deploy_world(); + let uri = world.metadata_uri(); + + assert(uri.len() == 1, 'Incorrect metadata uri len'); + assert(uri[0] == @'test_uri', 'Incorrect metadata uri'); + + world.set_metadata_uri(array!['new_uri', 'longer'].span()); + + let uri = world.metadata_uri(); + 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'); +} + +#[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(array!['new_uri', 'longer'].span()); +} + #[test] #[available_gas(60000000)] fn test_entities() { @@ -191,7 +220,6 @@ fn test_entities() { let layout = array![251].span(); let (keys, values) = world.entities('Foo', Option::None(()), query_keys, 2, layout); assert(keys.len() == 1, 'No keys found for any!'); - // query_keys.append(0x1337); // let (keys, values) = world.entities('Foo', 42, query_keys.span(), 2, layout); // assert(keys.len() == 1, 'No keys found!'); @@ -368,4 +396,3 @@ fn test_execute_multiple_worlds() { assert(data2.a == 7331, 'data2 not stored'); } - diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index 140593912f..76b10a5443 100644 --- a/crates/dojo-lang/src/manifest_test_data/manifest +++ b/crates/dojo-lang/src/manifest_test_data/manifest @@ -8,7 +8,7 @@ test_manifest_file "world": { "name": "world", "address": null, - "class_hash": "0x45539636cfa153f8ea0975102b04d35f455240c98c1dcf1eeb0013bef358d35", + "class_hash": "0x7cb53315435063879e8aa2487ead65eb1196ac71aa7f2888e9762ac430400d", "abi": [ { "type": "impl", @@ -77,6 +77,29 @@ test_manifest_file "type": "interface", "name": "dojo::world::IWorld", "items": [ + { + "type": "function", + "name": "metadata_uri", + "inputs": [], + "outputs": [ + { + "type": "core::array::Span::" + } + ], + "state_mutability": "view" + }, + { + "type": "function", + "name": "set_metadata_uri", + "inputs": [ + { + "name": "uri", + "type": "core::array::Span::" + } + ], + "outputs": [], + "state_mutability": "external" + }, { "type": "function", "name": "model", @@ -411,6 +434,10 @@ test_manifest_file { "name": "contract_base", "type": "core::starknet::class_hash::ClassHash" + }, + { + "name": "metadata_uri", + "type": "core::array::Span::" } ] }, @@ -428,6 +455,11 @@ test_manifest_file "name": "caller", "type": "core::starknet::contract_address::ContractAddress", "kind": "data" + }, + { + "name": "metadata_uri", + "type": "core::array::Span::", + "kind": "data" } ] }, diff --git a/crates/dojo-world/src/migration/strategy.rs b/crates/dojo-world/src/migration/strategy.rs index 5360d9088f..a8fdefc1d6 100644 --- a/crates/dojo-world/src/migration/strategy.rs +++ b/crates/dojo-world/src/migration/strategy.rs @@ -131,7 +131,11 @@ where world.contract_address = get_contract_address( salt, diff.world.local, - &[executor.as_ref().unwrap().contract_address, base.as_ref().unwrap().diff.local], + &[ + executor.as_ref().unwrap().contract_address, + base.as_ref().unwrap().diff.local, + FieldElement::ZERO, + ], FieldElement::ZERO, ); } diff --git a/crates/sozo/src/ops/migration/mod.rs b/crates/sozo/src/ops/migration/mod.rs index 3571825d21..0be15b4a89 100644 --- a/crates/sozo/src/ops/migration/mod.rs +++ b/crates/sozo/src/ops/migration/mod.rs @@ -298,6 +298,7 @@ where let calldata = vec![ strategy.executor.as_ref().unwrap().contract_address, strategy.base.as_ref().unwrap().diff.local, + FieldElement::ZERO, ]; deploy_contract(world, "world", calldata, migrator, ui, &txn_config).await?; diff --git a/crates/torii/client/Cargo.toml b/crates/torii/client/Cargo.toml index c1bc86f6c5..4320cf72d0 100644 --- a/crates/torii/client/Cargo.toml +++ b/crates/torii/client/Cargo.toml @@ -12,6 +12,7 @@ crypto-bigint = "0.5.3" dojo-types = { path = "../../dojo-types" } futures-util = "0.3.28" futures.workspace = true +http = "0.2.9" parking_lot.workspace = true serde.workspace = true serde_json.workspace = true diff --git a/crates/torii/client/src/contract/world.rs b/crates/torii/client/src/contract/world.rs index aa7ee4c91f..4c1e95b819 100644 --- a/crates/torii/client/src/contract/world.rs +++ b/crates/torii/client/src/contract/world.rs @@ -1,5 +1,6 @@ use std::result::Result; +use http::uri::{InvalidUri, Uri}; use starknet::accounts::{AccountError, Call, ConnectedAccount}; use starknet::core::types::{BlockId, FieldElement, FunctionCall, InvokeTransactionResult}; use starknet::core::utils::{ @@ -23,6 +24,8 @@ pub enum WorldContractError { CairoShortStringToFeltError(CairoShortStringToFeltError), #[error(transparent)] ContractReaderError(ContractReaderError

), + #[error("Invalid metadata uri")] + InvalidMetadataUri(InvalidUri), } #[derive(Debug)] @@ -52,6 +55,38 @@ impl<'a, A: ConnectedAccount + Sync> WorldContract<'a, A> { .await } + pub async fn set_metadata_uri( + &self, + metadata_uri: String, + ) -> Result< + InvokeTransactionResult, + WorldContractError::Error>, + > { + let parsed: Uri = + metadata_uri.try_into().map_err(WorldContractError::InvalidMetadataUri)?; + + let encoded = parsed + .to_string() + .chars() + .collect::>() + .chunks(31) + .map(|chunk| { + let s: String = chunk.iter().collect(); + cairo_short_string_to_felt(&s).unwrap() + }) + .collect::>(); + + self.account + .execute(vec![Call { + calldata: encoded, + to: self.address, + selector: get_selector_from_name("set_metadata_uri").unwrap(), + }]) + .send() + .await + .map_err(WorldContractError::AccountError) + } + pub async fn grant_writer( &self, model: &str, @@ -221,6 +256,26 @@ impl<'a, P: Provider + Sync> WorldContractReader<'a, P> { Ok(res[0]) } + pub async fn metadata_uri( + &self, + block_id: BlockId, + ) -> Result> { + let res = self + .provider + .call( + FunctionCall { + contract_address: self.address, + calldata: vec![], + entry_point_selector: get_selector_from_name("metadata_uri").unwrap(), + }, + block_id, + ) + .await + .map_err(ContractReaderError::ProviderError)?; + + Ok(res[0]) + } + pub async fn base( &self, block_id: BlockId, diff --git a/crates/torii/client/src/contract/world_test.rs b/crates/torii/client/src/contract/world_test.rs index becf7c836d..fdfa64973d 100644 --- a/crates/torii/client/src/contract/world_test.rs +++ b/crates/torii/client/src/contract/world_test.rs @@ -65,7 +65,7 @@ pub async fn deploy_world( .unwrap() .deploy( manifest.clone().world.class_hash, - vec![executor_address, base_class_hash], + vec![executor_address, base_class_hash, FieldElement::ZERO], &account, Default::default(), ) diff --git a/crates/torii/client/wasm/Cargo.lock b/crates/torii/client/wasm/Cargo.lock index 21c9a5335c..f158ab4811 100644 --- a/crates/torii/client/wasm/Cargo.lock +++ b/crates/torii/client/wasm/Cargo.lock @@ -3056,6 +3056,7 @@ dependencies = [ "dojo-types", "futures", "futures-util", + "http", "js-sys", "parking_lot 0.12.1", "prost 0.11.9",