diff --git a/bin/sozo/src/commands/auth.rs b/bin/sozo/src/commands/auth.rs index 67ab7a5016..aa76060082 100644 --- a/bin/sozo/src/commands/auth.rs +++ b/bin/sozo/src/commands/auth.rs @@ -156,7 +156,7 @@ pub async fn grant( contracts=?models_contracts, "Granting Writer permissions." ); - auth::grant_writer(ui, &world, models_contracts, transaction.into(), default_namespace) + auth::grant_writer(ui, &world, &models_contracts, transaction.into(), default_namespace) .await } AuthKind::Owner { owners_resources } => { @@ -164,7 +164,7 @@ pub async fn grant( resources=?owners_resources, "Granting Owner permissions." ); - auth::grant_owner(ui, &world, owners_resources, transaction.into(), default_namespace) + auth::grant_owner(ui, &world, &owners_resources, transaction.into(), default_namespace) .await } } @@ -192,15 +192,21 @@ pub async fn revoke( contracts=?models_contracts, "Revoking Writer permissions." ); - auth::revoke_writer(ui, &world, models_contracts, transaction.into(), default_namespace) - .await + auth::revoke_writer( + ui, + &world, + &models_contracts, + transaction.into(), + default_namespace, + ) + .await } AuthKind::Owner { owners_resources } => { trace!( resources=?owners_resources, "Revoking Owner permissions." ); - auth::revoke_owner(ui, &world, owners_resources, transaction.into(), default_namespace) + auth::revoke_owner(ui, &world, &owners_resources, transaction.into(), default_namespace) .await } } diff --git a/bin/sozo/tests/register_test.rs b/bin/sozo/tests/register_test.rs index ce0f84d83f..b882b873c9 100644 --- a/bin/sozo/tests/register_test.rs +++ b/bin/sozo/tests/register_test.rs @@ -41,7 +41,7 @@ async fn reregister_models() { account.set_block_id(BlockId::Tag(BlockTag::Pending)); execute_strategy(&ws, &migration, &account, TxnConfig::init_wait()).await.unwrap(); - let world_address = &format!("0x{:x}", &migration.world_address().unwrap()); + let world_address = &format!("0x{:x}", &migration.world_address); let account_address = &format!("0x{:x}", account.address()); let private_key = &format!("0x{:x}", sequencer.account_data(0).private_key.as_ref().unwrap().secret_scalar()); diff --git a/crates/dojo-test-utils/src/migration.rs b/crates/dojo-test-utils/src/migration.rs index fff72ec87c..37c9a8e550 100644 --- a/crates/dojo-test-utils/src/migration.rs +++ b/crates/dojo-test-utils/src/migration.rs @@ -32,11 +32,9 @@ pub fn prepare_migration( manifest.merge(overlay_manifest); } - let mut world = WorldDiff::compute(manifest, None); - world.update_order(default_namespace).unwrap(); + let world = WorldDiff::compute(manifest, None, default_namespace)?; - let mut strat = prepare_for_migration(None, felt!("0x12345"), &target_dir, world).unwrap(); - strat.resolve_variable(strat.world_address().unwrap()).unwrap(); + let strat = prepare_for_migration(None, felt!("0x12345"), &target_dir, world).unwrap(); Ok(strat) } @@ -47,7 +45,7 @@ pub fn prepare_migration_with_world_and_seed( world_address: Option, seed: &str, default_namespace: &str, -) -> Result { +) -> Result<(MigrationStrategy, WorldDiff)> { // In testing, profile name is always dev. let profile_name = "dev"; @@ -62,9 +60,9 @@ pub fn prepare_migration_with_world_and_seed( manifest.merge(overlay_manifest); } - let mut world = WorldDiff::compute(manifest, None); - world.update_order(default_namespace).unwrap(); + let world = WorldDiff::compute(manifest, None, default_namespace)?; let seed = cairo_short_string_to_felt(seed).unwrap(); - prepare_for_migration(world_address, seed, &target_dir, world) + let strat = prepare_for_migration(world_address, seed, &target_dir, world.clone())?; + Ok((strat, world)) } diff --git a/crates/dojo-world/src/contracts/naming.rs b/crates/dojo-world/src/contracts/naming.rs index 93bd561b3a..5acdfe1189 100644 --- a/crates/dojo-world/src/contracts/naming.rs +++ b/crates/dojo-world/src/contracts/naming.rs @@ -68,6 +68,18 @@ pub fn get_filename_from_tag(tag: &str) -> String { format!("{tag}{TAG_SEPARATOR}{selector}") } +pub fn get_tag_from_filename(filename: &str) -> Result { + let parts: Vec<&str> = filename.split(TAG_SEPARATOR).collect(); + if parts.len() != 3 { + return Err(anyhow!( + "Unexpected filename. Expected format: \ + {TAG_SEPARATOR}{TAG_SEPARATOR}" + )); + } + + Ok(format!("{}{TAG_SEPARATOR}{}", parts[0], parts[1])) +} + pub fn compute_bytearray_hash(value: &str) -> Felt { let ba = ByteArray::from_string(value).unwrap(); poseidon_hash_many(&ByteArray::cairo_serialize(&ba)) diff --git a/crates/dojo-world/src/contracts/world_test.rs b/crates/dojo-world/src/contracts/world_test.rs index e6e2b46520..16e0955b13 100644 --- a/crates/dojo-world/src/contracts/world_test.rs +++ b/crates/dojo-world/src/contracts/world_test.rs @@ -75,14 +75,12 @@ pub async fn deploy_world( manifest.merge(overlay_manifest); } - let mut world = WorldDiff::compute(manifest.clone(), None); - world.update_order(default_namespace).unwrap(); + let world = WorldDiff::compute(manifest.clone(), None, default_namespace).unwrap(); let account = sequencer.account(0); - let mut strategy = + let strategy = prepare_for_migration(None, Felt::from_hex("0x12345").unwrap(), target_dir, world).unwrap(); - strategy.resolve_variable(strategy.world_address().unwrap()).unwrap(); let base_class_hash = strategy.base.unwrap().declare(&account, &TxnConfig::init_wait()).await.unwrap().class_hash; diff --git a/crates/dojo-world/src/manifest/manifest_test.rs b/crates/dojo-world/src/manifest/manifest_test.rs index f59b85063a..fea5baabc7 100644 --- a/crates/dojo-world/src/manifest/manifest_test.rs +++ b/crates/dojo-world/src/manifest/manifest_test.rs @@ -136,7 +136,7 @@ fn parse_deployed_contracts_events_without_upgrade() { build_deploy_event(vec![felt!("0x0"), felt!("0x3"), felt!("0x789")], "ns3", "c3"), ]; - let actual_contracts = parse_contracts_events(events, vec![]); + let actual_contracts = parse_contracts_events(events, vec![], vec![]); assert_eq!(actual_contracts, expected_contracts); } @@ -213,7 +213,7 @@ fn parse_deployed_contracts_events_with_upgrade() { }, ]; - let actual_contracts = parse_contracts_events(deployed_events, upgrade_events); + let actual_contracts = parse_contracts_events(deployed_events, upgrade_events, vec![]); similar_asserts::assert_eq!(actual_contracts, expected_contracts); } @@ -293,7 +293,7 @@ fn events_without_block_number_arent_parsed() { }, ]; - let actual_contracts = parse_contracts_events(deployed_events, upgrade_events); + let actual_contracts = parse_contracts_events(deployed_events, upgrade_events, vec![]); similar_asserts::assert_eq!(actual_contracts, expected_contracts); } @@ -357,7 +357,7 @@ fn fetch_remote_manifest() { // compute diff from local and remote manifest - let diff = WorldDiff::compute(local_manifest, Some(remote_manifest)); + let diff = WorldDiff::compute(local_manifest, Some(remote_manifest), "dojo-test").unwrap(); assert_eq!(diff.count_diffs(), 0, "there should not be any diff"); } diff --git a/crates/dojo-world/src/manifest/mod.rs b/crates/dojo-world/src/manifest/mod.rs index 4200fd2e14..ca50783a2b 100644 --- a/crates/dojo-world/src/manifest/mod.rs +++ b/crates/dojo-world/src/manifest/mod.rs @@ -3,7 +3,7 @@ use std::path::PathBuf; use std::{fs, io}; use anyhow::Result; -use cainome::cairo_serde::{ByteArray, CairoSerde, Error as CainomeError}; +use cainome::cairo_serde::{ByteArray, CairoSerde, Error as CainomeError, Zeroable}; use camino::Utf8PathBuf; use serde::de::DeserializeOwned; use serde::Serialize; @@ -132,6 +132,24 @@ impl BaseManifest { self.models.retain(|model| !tags.contains(&model.inner.tag)); } + /// Generates a map of `tag -> ManifestKind` + pub fn build_kind_from_tags(&self) -> HashMap { + let mut kind_from_tags = HashMap::::new(); + + kind_from_tags.insert(WORLD_CONTRACT_TAG.to_string(), ManifestKind::WorldClass); + kind_from_tags.insert(BASE_CONTRACT_TAG.to_string(), ManifestKind::BaseClass); + + for model in self.models.as_slice() { + kind_from_tags.insert(model.inner.tag.clone(), ManifestKind::Model); + } + + for contract in self.contracts.as_slice() { + kind_from_tags.insert(contract.inner.tag.clone(), ManifestKind::Contract); + } + + kind_from_tags + } + pub fn merge(&mut self, overlay: OverlayManifest) { let mut base_map = HashMap::new(); @@ -161,7 +179,7 @@ impl BaseManifest { } #[derive(Clone, Debug, Copy)] -enum ManifestKind { +pub enum ManifestKind { BaseClass, WorldClass, Contract, @@ -169,23 +187,6 @@ enum ManifestKind { } impl OverlayManifest { - fn build_kind_from_tags(base_manifest: &BaseManifest) -> HashMap { - let mut kind_from_tags = HashMap::::new(); - - kind_from_tags.insert(WORLD_CONTRACT_TAG.to_string(), ManifestKind::WorldClass); - kind_from_tags.insert(BASE_CONTRACT_TAG.to_string(), ManifestKind::BaseClass); - - for model in base_manifest.models.as_slice() { - kind_from_tags.insert(model.inner.tag.clone(), ManifestKind::Model); - } - - for contract in base_manifest.contracts.as_slice() { - kind_from_tags.insert(contract.inner.tag.clone(), ManifestKind::Contract); - } - - kind_from_tags - } - fn load_overlay( path: &PathBuf, kind: ManifestKind, @@ -219,7 +220,7 @@ impl OverlayManifest { ) -> Result { fs::create_dir_all(path)?; - let kind_from_tags = Self::build_kind_from_tags(base_manifest); + let kind_from_tags = base_manifest.build_kind_from_tags(); let mut loaded_tags = HashMap::::new(); let mut overlays = OverlayManifest::default(); @@ -360,6 +361,7 @@ impl DeploymentManifest { Ok(()) } + // Writes the Deployment manifest in JSON format, with ABIs embedded. pub fn write_to_path_json(&self, path: &Utf8PathBuf, root_dir: &Utf8PathBuf) -> Result<()> { fs::create_dir_all(path.parent().unwrap())?; @@ -442,6 +444,23 @@ impl DeploymentManifest { } } +// impl DeploymentMetadata { +// pub fn load_from_path(path: &Utf8PathBuf) -> Result { +// let manifest: Self = toml::from_str(&fs::read_to_string(path)?).unwrap(); + +// Ok(manifest) +// } + +// pub fn write_to_path_toml(&self, path: &Utf8PathBuf) -> Result<()> { +// fs::create_dir_all(path.parent().unwrap())?; + +// let deployed_manifest = toml::to_string_pretty(&self)?; +// fs::write(path, deployed_manifest)?; + +// Ok(()) +// } +// } + // TODO: currently implementing this method using trait is causing lifetime issue due to // `async_trait` macro which is hard to debug. So moved it as a async method on type itself. // #[async_trait] @@ -465,6 +484,7 @@ where let registered_models_event_name = starknet_keccak("ModelRegistered".as_bytes()); let contract_deployed_event_name = starknet_keccak("ContractDeployed".as_bytes()); let contract_upgraded_event_name = starknet_keccak("ContractUpgraded".as_bytes()); + let writer_updated_event_name = starknet_keccak("WriterUpdated".as_bytes()); let events = get_events( &provider, @@ -473,6 +493,7 @@ where registered_models_event_name, contract_deployed_event_name, contract_upgraded_event_name, + writer_updated_event_name, ]], ) .await?; @@ -480,6 +501,7 @@ where let mut registered_models_events = vec![]; let mut contract_deployed_events = vec![]; let mut contract_upgraded_events = vec![]; + let mut writer_updated_events = vec![]; for event in events { match event.keys.first() { @@ -492,12 +514,19 @@ where Some(event_name) if *event_name == contract_upgraded_event_name => { contract_upgraded_events.push(event) } + Some(event_name) if *event_name == writer_updated_event_name => { + writer_updated_events.push(event) + } _ => {} } } let models = parse_models_events(registered_models_events); - let mut contracts = parse_contracts_events(contract_deployed_events, contract_upgraded_events); + let mut contracts = parse_contracts_events( + contract_deployed_events, + contract_upgraded_events, + writer_updated_events, + ); for contract in &mut contracts { contract.manifest_name = naming::get_filename_from_tag(&contract.inner.tag); @@ -537,7 +566,52 @@ async fn get_events( fn parse_contracts_events( deployed: Vec, upgraded: Vec, + granted: Vec, ) -> Vec> { + fn retain_only_latest_grant_events(events: Vec) -> HashMap> { + // create a map with some extra data which will be flattened later + // system -> (block_num, (resource -> perm)) + let mut grants: HashMap)> = HashMap::new(); + events.into_iter().for_each(|event| { + let mut data = event.data.into_iter(); + let block_num = event.block_number; + let resource = data.next().expect("resource is missing from event"); + let contract = data.next().expect("contract is missing from event"); + let value = data.next().expect("value is missing from event"); + + let value = !value.is_zero(); + + // Events that do not have a block number are ignored because we are unable to evaluate + // whether the events happened before or after the latest event that has been processed. + if let Some(num) = block_num { + grants + .entry(contract) + .and_modify(|(current_block, current_resource)| { + if *current_block < num { + *current_block = num; + current_resource.insert(resource, value); + } + }) + .or_insert((num, HashMap::from([(resource, value)]))); + } + }); + + // flatten out the map to remove block_number information and only include resources that + // are true i.e. system -> [resources] + grants + .into_iter() + .map(|(contract, (_, resources))| { + ( + contract, + resources + .into_iter() + .filter_map(|(resource, bool)| if bool { Some(resource) } else { None }) + .collect(), + ) + }) + .collect() + } + fn retain_only_latest_upgrade_events(events: Vec) -> HashMap { // addr -> (block_num, class_hash) let mut upgrades: HashMap = HashMap::new(); @@ -568,6 +642,7 @@ fn parse_contracts_events( } let upgradeds = retain_only_latest_upgrade_events(upgraded); + let grants = retain_only_latest_grant_events(granted); deployed .into_iter() @@ -594,12 +669,18 @@ fn parse_contracts_events( class_hash = *upgrade; } + let mut writes = vec![]; + if let Some(contract) = grants.get(&address) { + writes.extend(contract.iter().map(|f| f.to_hex_string())); + } + Manifest::new( DojoContract { address: Some(address), class_hash, abi: None, tag: tag.clone(), + writes, ..Default::default() }, naming::get_filename_from_tag(&tag), diff --git a/crates/dojo-world/src/manifest/types.rs b/crates/dojo-world/src/manifest/types.rs index f9d51b8326..5de7cdbf65 100644 --- a/crates/dojo-world/src/manifest/types.rs +++ b/crates/dojo-world/src/manifest/types.rs @@ -32,6 +32,8 @@ pub struct BaseManifest { pub struct DeploymentManifest { pub world: Manifest, pub base: Manifest, + // NOTE: `writes` field in contracts is of String but we read the values which are resource + // hashes from the events, so needs to be handled accordingly pub contracts: Vec>, pub models: Vec>, } diff --git a/crates/dojo-world/src/migration/contract.rs b/crates/dojo-world/src/migration/contract.rs index 2339454f1a..7cbc78eba5 100644 --- a/crates/dojo-world/src/migration/contract.rs +++ b/crates/dojo-world/src/migration/contract.rs @@ -18,6 +18,8 @@ pub struct ContractDiff { pub base_class_hash: Felt, pub remote_class_hash: Option, pub init_calldata: Vec, + pub local_writes: Vec, + pub remote_writes: Vec, } impl StateDiff for ContractDiff { diff --git a/crates/dojo-world/src/migration/strategy.rs b/crates/dojo-world/src/migration/strategy.rs index 3e221d27ba..fcce6e6f82 100644 --- a/crates/dojo-world/src/migration/strategy.rs +++ b/crates/dojo-world/src/migration/strategy.rs @@ -22,7 +22,7 @@ pub enum MigrationMetadata { #[derive(Debug, Clone)] pub struct MigrationStrategy { - pub world_address: Option, + pub world_address: Felt, pub world: Option, pub base: Option, pub contracts: Vec, @@ -37,13 +37,6 @@ pub struct MigrationItemsInfo { } impl MigrationStrategy { - pub fn world_address(&self) -> Result { - match &self.world { - Some(c) => Ok(c.contract_address), - None => self.world_address.ok_or(anyhow!("World address not found")), - } - } - pub fn info(&self) -> MigrationItemsInfo { let mut new = 0; let mut update = 0; @@ -145,7 +138,15 @@ pub fn prepare_for_migration( world.contract_address = generated_world_address; } - Ok(MigrationStrategy { world_address, world, base, contracts, models, metadata }) + // If world address is not provided, then we expect the world to be migrated. + let world_address = world_address.unwrap_or_else(|| world.as_ref().unwrap().contract_address); + + let mut migration = + MigrationStrategy { world_address, world, base, contracts, models, metadata }; + + migration.resolve_variable(world_address)?; + + Ok(migration) } fn evaluate_models_to_migrate( diff --git a/crates/dojo-world/src/migration/world.rs b/crates/dojo-world/src/migration/world.rs index 9a703f1338..46d76f5569 100644 --- a/crates/dojo-world/src/migration/world.rs +++ b/crates/dojo-world/src/migration/world.rs @@ -28,7 +28,11 @@ pub struct WorldDiff { } impl WorldDiff { - pub fn compute(local: BaseManifest, remote: Option) -> WorldDiff { + pub fn compute( + local: BaseManifest, + remote: Option, + default_namespace: &str, + ) -> Result { let models = local .models .iter() @@ -70,6 +74,16 @@ impl WorldDiff { .map(|r| *r.inner.class_hash()) }), init_calldata: contract.inner.init_calldata.clone(), + local_writes: contract.inner.writes.clone(), + remote_writes: remote + .as_ref() + .and_then(|m| { + m.contracts + .iter() + .find(|r| r.inner.class_hash() == contract.inner.class_hash()) + .map(|r| r.inner.writes.clone()) + }) + .unwrap_or_default(), } }) .collect::>(); @@ -88,9 +102,14 @@ impl WorldDiff { base_class_hash: *local.base.inner.class_hash(), remote_class_hash: remote.map(|m| *m.world.inner.class_hash()), init_calldata: vec![], + local_writes: vec![], + remote_writes: vec![], }; - WorldDiff { world, base, contracts, models } + let mut diff = WorldDiff { world, base, contracts, models }; + diff.update_order(default_namespace)?; + + Ok(diff) } pub fn count_diffs(&self) -> usize { diff --git a/crates/dojo-world/src/migration/world_test.rs b/crates/dojo-world/src/migration/world_test.rs index 94c85a51ef..c6a0c8b8fa 100644 --- a/crates/dojo-world/src/migration/world_test.rs +++ b/crates/dojo-world/src/migration/world_test.rs @@ -32,7 +32,7 @@ fn no_diff_when_local_and_remote_are_equal() { let mut remote: DeploymentManifest = local.clone().into(); remote.models = remote_models; - let diff = WorldDiff::compute(local, Some(remote)); + let diff = WorldDiff::compute(local, Some(remote), "dojo-test").unwrap(); assert_eq!(diff.count_diffs(), 0); } @@ -121,7 +121,7 @@ fn diff_when_local_and_remote_are_different() { remote.models[1].inner.class_hash = 33_u32.into(); remote.contracts[0].inner.class_hash = felt!("0x1112"); - let diff = WorldDiff::compute(local, Some(remote)); + let diff = WorldDiff::compute(local, Some(remote), "dojo-test").unwrap(); assert_eq!(diff.count_diffs(), 3); assert!(diff.models.iter().any(|m| m.tag == get_tag("dojo_mock", "model2"))); diff --git a/crates/sozo/ops/src/auth.rs b/crates/sozo/ops/src/auth.rs index fb8fcc9e5b..dd66c36183 100644 --- a/crates/sozo/ops/src/auth.rs +++ b/crates/sozo/ops/src/auth.rs @@ -20,6 +20,8 @@ pub enum ResourceType { Contract(String), Namespace(String), Model(String), + // this can be a selector for any other resource type + Selector(Felt), } impl FromStr for ResourceType { @@ -35,10 +37,14 @@ impl FromStr for ResourceType { Some(("namespace", name)) | Some(("ns", name)) => { ResourceType::Namespace(name.to_string()) } - _ => anyhow::bail!( + Some(("selector", name)) | Some(("s", name)) => { + ResourceType::Selector(Felt::from_str(name)?) + } + _ => anyhow::bail!(format!( "Resource is expected to be in the format `resource_type:resource_name`: `sozo \ - auth grant owner resource_type:resource_name,0x1234`" - ), + auth grant owner resource_type:resource_name,0x1234`, Found: {}.", + s + )), }; Ok(resource) } @@ -101,7 +107,7 @@ impl FromStr for ResourceOwner { pub async fn grant_writer<'a, A>( ui: &'a Ui, world: &WorldContract, - new_writers: Vec, + new_writers: &[ResourceWriter], txn_config: TxnConfig, default_namespace: &str, ) -> Result<()> @@ -115,7 +121,7 @@ where let resource_selector = get_resource_selector(ui, world, &new_writer.resource, default_namespace).await?; let contract_address = - utils::get_contract_address(world, new_writer.tag_or_address).await?; + utils::get_contract_address(world, &new_writer.tag_or_address).await?; calls.push(world.grant_writer_getcall(&resource_selector, &contract_address.into())); } @@ -143,7 +149,7 @@ where pub async fn grant_owner( ui: &Ui, world: &WorldContract, - new_owners: Vec, + new_owners: &[ResourceOwner], txn_config: TxnConfig, default_namespace: &str, ) -> Result<()> @@ -180,7 +186,7 @@ where pub async fn revoke_writer( ui: &Ui, world: &WorldContract, - new_writers: Vec, + new_writers: &[ResourceWriter], txn_config: TxnConfig, default_namespace: &str, ) -> Result<()> @@ -193,7 +199,7 @@ where let resource_selector = get_resource_selector(ui, world, &new_writer.resource, default_namespace).await?; let contract_address = - utils::get_contract_address(world, new_writer.tag_or_address).await?; + utils::get_contract_address(world, &new_writer.tag_or_address).await?; calls.push(world.revoke_writer_getcall(&resource_selector, &contract_address.into())); } @@ -221,7 +227,7 @@ where pub async fn revoke_owner( ui: &Ui, world: &WorldContract, - new_owners: Vec, + new_owners: &[ResourceOwner], txn_config: TxnConfig, default_namespace: &str, ) -> Result<()> @@ -255,7 +261,7 @@ where Ok(()) } -async fn get_resource_selector( +pub async fn get_resource_selector( ui: &Ui, world: &WorldContract, resource: &ResourceType, @@ -275,7 +281,7 @@ where } else { ensure_namespace(tag_or_address, default_namespace) }; - utils::get_contract_address(world, tag_or_address).await? + utils::get_contract_address(world, &tag_or_address).await? } ResourceType::Model(tag_or_name) => { // TODO: Is some models have version 0 (using the name of the struct instead of the @@ -297,6 +303,7 @@ where compute_selector_from_tag(&tag) } ResourceType::Namespace(name) => compute_bytearray_hash(name), + ResourceType::Selector(selector) => *selector, }; Ok(resource_selector) diff --git a/crates/sozo/ops/src/execute.rs b/crates/sozo/ops/src/execute.rs index 24dd6c6ee2..ce91bd9d1c 100644 --- a/crates/sozo/ops/src/execute.rs +++ b/crates/sozo/ops/src/execute.rs @@ -20,7 +20,7 @@ pub async fn execute( where A: ConnectedAccount + Sync + Send + 'static, { - let contract_address = utils::get_contract_address(world, tag_or_address).await?; + let contract_address = utils::get_contract_address(world, &tag_or_address).await?; let res = world .account .execute_v1(vec![Call { diff --git a/crates/sozo/ops/src/migration/auto_auth.rs b/crates/sozo/ops/src/migration/auto_auth.rs index 54eb7af458..ae64a67e6f 100644 --- a/crates/sozo/ops/src/migration/auto_auth.rs +++ b/crates/sozo/ops/src/migration/auto_auth.rs @@ -1,73 +1,28 @@ -use std::str::FromStr; - use anyhow::Result; use dojo_world::contracts::WorldContract; -use dojo_world::manifest::BaseManifest; use dojo_world::migration::TxnConfig; use scarb::core::Workspace; -use scarb_ui::Ui; use starknet::accounts::ConnectedAccount; -use super::ui::MigrationUi; -use super::MigrationOutput; -use crate::auth::{grant_writer, ResourceWriter}; +use crate::auth::{grant_writer, revoke_writer, ResourceWriter}; pub async fn auto_authorize( ws: &Workspace<'_>, world: &WorldContract, txn_config: &TxnConfig, - local_manifest: &BaseManifest, - migration_output: &MigrationOutput, default_namespace: &str, + grant: &[ResourceWriter], + revoke: &[ResourceWriter], ) -> Result<()> where - A: ConnectedAccount + Sync + Send, + A: ConnectedAccount + Sync + Send + 'static, A::SignError: 'static, { let ui = ws.config().ui(); ui.print(" "); - ui.print_step(6, "🖋️", "Authorizing systems based on overlay..."); - ui.print(" "); - let new_writers = compute_writers(&ui, local_manifest, migration_output)?; - grant_writer(&ui, world, new_writers, *txn_config, default_namespace).await -} - -pub fn compute_writers( - ui: &Ui, - local_manifest: &BaseManifest, - migration_output: &MigrationOutput, -) -> Result> { - let mut res = vec![]; - let local_contracts = &local_manifest.contracts; - - // From all the contracts that were migrated successfully. - for migrated_contract in migration_output.contracts.iter().flatten() { - // Find that contract from local_manifest based on its tag. - let contract = local_contracts - .iter() - .find(|c| migrated_contract.tag == c.inner.tag) - .expect("we know this contract exists"); - - if !contract.inner.writes.is_empty() { - ui.print_sub(format!( - "Authorizing {} for resources: {:?}", - contract.inner.tag, contract.inner.writes - )); - } - - for tag_with_prefix in &contract.inner.writes { - let resource_type = if tag_with_prefix.contains(':') { - tag_with_prefix.to_string() - } else { - // Default to model if no prefix was given. - format!("m:{}", tag_with_prefix) - }; - - let resource = format!("{},{}", resource_type, migrated_contract.tag); - res.push(ResourceWriter::from_str(&resource)?); - } - } + grant_writer(&ui, world, grant, *txn_config, default_namespace).await?; + revoke_writer(&ui, world, revoke, *txn_config, default_namespace).await?; - Ok(res) + Ok(()) } diff --git a/crates/sozo/ops/src/migration/migrate.rs b/crates/sozo/ops/src/migration/migrate.rs index dd5bd57718..a5fa60ab2d 100644 --- a/crates/sozo/ops/src/migration/migrate.rs +++ b/crates/sozo/ops/src/migration/migrate.rs @@ -1,4 +1,6 @@ +use std::collections::{HashMap, HashSet}; use std::path::Path; +use std::str::FromStr; use anyhow::{anyhow, bail, Context, Result}; use cainome::cairo_serde::ByteArray; @@ -26,7 +28,7 @@ use futures::future; use itertools::Itertools; use scarb::core::Workspace; use scarb_ui::Ui; -use starknet::accounts::ConnectedAccount; +use starknet::accounts::{Account, ConnectedAccount}; use starknet::core::types::{ BlockId, BlockTag, Felt, FunctionCall, InvokeTransactionResult, StarknetError, }; @@ -37,9 +39,11 @@ use starknet::providers::{Provider, ProviderError}; use tokio::fs; use super::ui::{bold_message, italic_message, MigrationUi}; +use super::utils::generate_resource_map; use super::{ ContractDeploymentOutput, ContractMigrationOutput, ContractUpgradeOutput, MigrationOutput, }; +use crate::auth::{get_resource_selector, ResourceType, ResourceWriter}; pub fn prepare_migration( target_dir: &Utf8PathBuf, @@ -71,7 +75,7 @@ pub async fn apply_diff( ws: &Workspace<'_>, account: A, txn_config: TxnConfig, - strategy: &mut MigrationStrategy, + strategy: &MigrationStrategy, ) -> Result where A: ConnectedAccount + Sync + Send, @@ -93,27 +97,18 @@ where ui.print(format!( "\n🎉 Successfully migrated World on block #{} at address {}\n", block_number, - bold_message(format!( - "{:#x}", - strategy.world_address().expect("world address must exist") - )) + bold_message(format!("{:#x}", strategy.world_address)) )); } else { ui.print(format!( "\n🎉 Successfully migrated World at address {}\n", - bold_message(format!( - "{:#x}", - strategy.world_address().expect("world address must exist") - )) + bold_message(format!("{:#x}", strategy.world_address)) )); } } else { ui.print(format!( "\n🚨 Partially migrated World at address {}", - bold_message(format!( - "{:#x}", - strategy.world_address().expect("world address must exist") - )) + bold_message(format!("{:#x}", strategy.world_address)) )); } @@ -207,8 +202,9 @@ where None => {} }; + let world_address = strategy.world_address; let mut migration_output = MigrationOutput { - world_address: strategy.world_address()?, + world_address, world_tx_hash, world_block_number, full: false, @@ -216,8 +212,6 @@ where contracts: vec![], }; - let world_address = strategy.world_address()?; - // register namespaces let mut namespaces = strategy.models.iter().map(|m| get_namespace_from_tag(&m.diff.tag)).collect::>(); @@ -261,13 +255,13 @@ where /// into the Dojo resource registry. /// /// # Arguments -/// * `element_name` - fully qualified name of the element linked to the metadata -/// * `resource_id` - the id of the resource to create. -/// * `artifact` - the artifact to upload on IPFS. +/// * `ui` - The user interface object for displaying information +/// * `resource_id` - The id of the resource to create +/// * `metadata` - The ResourceMetadata object containing the metadata to upload /// /// # Returns -/// A [`ResourceData`] object to register in the Dojo resource register -/// on success. +/// A [`world::ResourceMetadata`] object to register in the Dojo resource register +/// on success, or an error if the upload fails. async fn upload_on_ipfs_and_create_resource( ui: &Ui, resource_id: Felt, @@ -701,42 +695,38 @@ pub fn handle_artifact_error(ui: &Ui, artifact_path: &Path, error: anyhow::Error pub async fn get_contract_operation_name

( provider: P, contract: &ContractMigration, - world_address: Option, + world_address: Felt, ) -> String where P: Provider + Sync + Send, { - if let Some(world_address) = world_address { - if let Ok(base_class_hash) = provider - .call( - FunctionCall { - contract_address: world_address, - calldata: vec![], - entry_point_selector: get_selector_from_name("base").unwrap(), - }, - BlockId::Tag(BlockTag::Pending), - ) - .await - { - let contract_address = - get_contract_address(contract.salt, base_class_hash[0], &[], world_address); + if let Ok(base_class_hash) = provider + .call( + FunctionCall { + contract_address: world_address, + calldata: vec![], + entry_point_selector: get_selector_from_name("base").unwrap(), + }, + BlockId::Tag(BlockTag::Pending), + ) + .await + { + let contract_address = + get_contract_address(contract.salt, base_class_hash[0], &[], world_address); - match provider - .get_class_hash_at(BlockId::Tag(BlockTag::Pending), contract_address) - .await - { - Ok(current_class_hash) if current_class_hash != contract.diff.local_class_hash => { - return format!("{}: Upgrade", contract.diff.tag); - } - Err(ProviderError::StarknetError(StarknetError::ContractNotFound)) => { - return format!("{}: Deploy", contract.diff.tag); - } - Ok(_) => return "Already Deployed".to_string(), - Err(_) => return format!("{}: Deploy", contract.diff.tag), + match provider.get_class_hash_at(BlockId::Tag(BlockTag::Pending), contract_address).await { + Ok(current_class_hash) if current_class_hash != contract.diff.local_class_hash => { + format!("{}: Upgrade", contract.diff.tag) + } + Err(ProviderError::StarknetError(StarknetError::ContractNotFound)) => { + format!("{}: Deploy", contract.diff.tag) } + Ok(_) => "Already Deployed".to_string(), + Err(_) => format!("{}: Deploy", contract.diff.tag), } + } else { + format!("{}: Deploy", contract.diff.tag) } - format!("deploy {}", contract.diff.tag) } pub async fn print_strategy

( @@ -831,6 +821,8 @@ pub async fn update_manifests_and_abis( // when the migration has not been applied because in `plan` mode or because of an error, // the `migration_output` is empty. if let Some(migration_output) = migration_output { + // update world deployment transaction hash or block number if they are present in the + // migration output if migration_output.world_tx_hash.is_some() { local_manifest.world.inner.transaction_hash = migration_output.world_tx_hash; } @@ -841,7 +833,7 @@ pub async fn update_manifests_and_abis( migration_output.contracts.iter().for_each(|contract_output| { // ignore failed migration which are represented by None if let Some(output) = contract_output { - // find the contract in local manifest and update its address and base class hash + // find the contract in local manifest and update its base class hash let local = local_manifest .contracts .iter_mut() @@ -853,6 +845,8 @@ pub async fn update_manifests_and_abis( }); } + // compute contract addresses and update them in the manifest for contracts + // that have a base class hash set. local_manifest.contracts.iter_mut().for_each(|contract| { if contract.inner.base_class_hash != Felt::ZERO { let salt = generate_salt(&get_name_from_tag(&contract.inner.tag)); @@ -865,8 +859,6 @@ pub async fn update_manifests_and_abis( } }); - // copy abi files from `base/abi` to `deployment/abi` and update abi path in - // local_manifest update_manifest_abis(&mut local_manifest, manifest_dir, profile_name).await; local_manifest @@ -883,6 +875,135 @@ pub async fn update_manifests_and_abis( Ok(()) } +// For now we juust handle writers, handling of owners might be added in the future +pub async fn find_authorization_diff( + ui: &Ui, + world: &WorldContract, + diff: &WorldDiff, + migration_output: Option<&MigrationOutput>, + default_namespace: &str, +) -> Result<(Vec, Vec)> +where + A: ConnectedAccount + Sync + Send, + ::SignError: 'static, +{ + let mut grant = vec![]; + let mut revoke = vec![]; + + let mut recently_migrated = HashSet::new(); + + if let Some(migration_output) = migration_output { + recently_migrated = migration_output + .contracts + .iter() + .flatten() + .map(|m| m.tag.clone()) + .collect::>() + } + + // Generate a map of `Felt` (resource selector) -> `ResourceType` that are available locally + // so we can check if the resource being revoked is known locally. + // + // if the selector is not found in the map we just print its selector + let resource_map = generate_resource_map(ui, world, diff).await?; + + for c in &diff.contracts { + // remote is none meants it was not previously deployed. + // but if it didn't get deployed even during this run we should skip migration for it + if c.remote_class_hash.is_none() && !recently_migrated.contains(&c.tag) { + ui.print_sub(format!("Skipping migration for contract {}", c.tag)); + continue; + } + + let mut local = HashMap::new(); + for write in &c.local_writes { + let write = + if write.contains(':') { write.to_string() } else { format!("m:{}", write) }; + + let resource = ResourceType::from_str(&write)?; + let selector = get_resource_selector(ui, world, &resource, default_namespace) + .await + .with_context(|| format!("Failed to get selector for {}", write))?; + + let resource_writer = ResourceWriter::from_str(&format!("{},{}", write, c.tag))?; + local.insert(selector, resource_writer); + } + + for write in &c.remote_writes { + // This value is fetched from onchain events, so we get them as felts + let selector = Felt::from_str(write).with_context(|| "Expected write to be a felt")?; + if local.remove(&selector).is_some() { + // do nothing for one which are already onchain + } else { + // revoke ones that are not present in local + assert!(Felt::from_str(write).is_ok()); + revoke.push(ResourceWriter::from_str(&format!("s:{},{}", write, c.tag))?); + } + } + + // apply remaining + local.iter().for_each(|(_, resource_writer)| { + grant.push(resource_writer.clone()); + }); + + let contract_grants: Vec<_> = + grant.iter().filter(|rw| rw.tag_or_address == c.tag).cloned().collect(); + if !contract_grants.is_empty() { + ui.print_sub(format!( + "Granting write access to {} for resources: {:?}", + c.tag, + contract_grants + .iter() + .map(|rw| { + let resource = &rw.resource; + match resource { + // Replace selector with appropriate resource type if present in + // resource_map + ResourceType::Selector(s) => resource_map + .get(&s.to_hex_string()) + .cloned() + .unwrap_or_else(|| rw.resource.clone()), + _ => resource.clone(), + } + }) + .collect::>() + )); + } + + let contract_revokes: Vec<_> = + revoke.iter().filter(|rw| rw.tag_or_address == c.tag).cloned().collect(); + if !contract_revokes.is_empty() { + ui.print_sub(format!( + "Revoking write access to {} for resources: {:?}", + c.tag, + contract_revokes + .iter() + .map(|rw| { + let resource = &rw.resource; + match resource { + // Replace selector with appropriate resource type if present in + // resource_map + ResourceType::Selector(s) => resource_map + .get(&s.to_hex_string()) + .cloned() + .unwrap_or_else(|| rw.resource.clone()), + _ => resource.clone(), + } + }) + .collect::>() + )); + } + + if !contract_grants.is_empty() || !contract_revokes.is_empty() { + ui.print(" "); + } + } + + Ok((grant, revoke)) +} + +// copy abi files from `base/abi` to `deployment/abi` and update abi path in +// local_manifest async fn update_manifest_abis( local_manifest: &mut DeploymentManifest, manifest_dir: &Utf8PathBuf, diff --git a/crates/sozo/ops/src/migration/mod.rs b/crates/sozo/ops/src/migration/mod.rs index fc420d8b76..c0efa102a6 100644 --- a/crates/sozo/ops/src/migration/mod.rs +++ b/crates/sozo/ops/src/migration/mod.rs @@ -20,7 +20,8 @@ mod utils; pub use self::auto_auth::auto_authorize; use self::migrate::update_manifests_and_abis; pub use self::migrate::{ - apply_diff, execute_strategy, prepare_migration, print_strategy, upload_metadata, + apply_diff, execute_strategy, find_authorization_diff, prepare_migration, print_strategy, + upload_metadata, }; use self::ui::MigrationUi; @@ -56,7 +57,7 @@ pub async fn migrate( skip_manifests: Option>, ) -> Result> where - A: ConnectedAccount + Sync + Send, + A: ConnectedAccount + Sync + Send + 'static, A::Provider: Send, A::SignError: 'static, { @@ -108,23 +109,26 @@ where // Calculate diff between local and remote World manifests. ui.print_step(2, "🧰", "Evaluating Worlds diff..."); - let mut diff = WorldDiff::compute(local_manifest.clone(), remote_manifest.clone()); - diff.update_order(&default_namespace)?; + let diff = + WorldDiff::compute(local_manifest.clone(), remote_manifest.clone(), &default_namespace)?; let total_diffs = diff.count_diffs(); ui.print_sub(format!("Total diffs found: {total_diffs}")); if total_diffs == 0 { - ui.print("\n✨ No changes to be made. Remote World is already up to date!"); - return Ok(None); + ui.print("\n✨ No diffs found. Remote World is already up to date!"); } - let mut strategy = prepare_migration(&target_dir, diff, name, world_address, &ui)?; - let world_address = strategy.world_address().expect("world address must exist"); - strategy.resolve_variable(world_address)?; - + let strategy = prepare_migration(&target_dir, diff.clone(), name, world_address, &ui)?; + // TODO: dry run can also show the diffs for things apart from world state + // what new authorizations would be granted, if ipfs data would change or not, + // etc... if dry_run { - print_strategy(&ui, account.provider(), &strategy, world_address).await; + if total_diffs == 0 { + return Ok(None); + } + + print_strategy(&ui, account.provider(), &strategy, strategy.world_address).await; update_manifests_and_abis( ws, @@ -132,7 +136,7 @@ where &manifest_dir, &profile_name, &rpc_url, - world_address, + strategy.world_address, None, name, ) @@ -140,23 +144,26 @@ where Ok(None) } else { - // Migrate according to the diff. - let migration_output = match apply_diff(ws, &account, txn_config, &mut strategy).await { - Ok(migration_output) => Some(migration_output), - Err(e) => { - update_manifests_and_abis( - ws, - local_manifest, - &manifest_dir, - &profile_name, - &rpc_url, - world_address, - None, - name, - ) - .await?; - return Err(e)?; + let migration_output = if total_diffs != 0 { + match apply_diff(ws, &account, txn_config, &strategy).await { + Ok(migration_output) => Some(migration_output), + Err(e) => { + update_manifests_and_abis( + ws, + local_manifest, + &manifest_dir, + &profile_name, + &rpc_url, + strategy.world_address, + None, + name, + ) + .await?; + return Err(e)?; + } } + } else { + None }; update_manifests_and_abis( @@ -165,34 +172,36 @@ where &manifest_dir, &profile_name, &rpc_url, - world_address, + strategy.world_address, migration_output.clone(), name, ) .await?; let account = Arc::new(account); - let world = WorldContract::new(world_address, account.clone()); - if let Some(migration_output) = &migration_output { - match auto_authorize( - ws, - &world, - &txn_config, - &local_manifest, - migration_output, - &default_namespace, - ) - .await - { - Ok(()) => { - ui.print_sub("Auto authorize completed successfully"); - } - Err(e) => { - ui.print_sub(format!("Failed to auto authorize with error: {e}")); - } - }; + let world = WorldContract::new(strategy.world_address, account.clone()); + + ui.print(" "); + ui.print_step(6, "🖋️", "Authorizing systems based on overlay..."); + let (grant, revoke) = find_authorization_diff( + &ui, + &world, + &diff, + migration_output.as_ref(), + &default_namespace, + ) + .await?; - // + match auto_authorize(ws, &world, &txn_config, &default_namespace, &grant, &revoke).await { + Ok(()) => { + ui.print_sub("Auto authorize completed successfully"); + } + Err(e) => { + ui.print_sub(format!("Failed to auto authorize with error: {e}")); + } + }; + + if let Some(migration_output) = &migration_output { if !ws.config().offline() { upload_metadata(ws, &account, migration_output.clone(), txn_config).await?; } diff --git a/crates/sozo/ops/src/migration/utils.rs b/crates/sozo/ops/src/migration/utils.rs index dc91f26c1f..4e2aedeff6 100644 --- a/crates/sozo/ops/src/migration/utils.rs +++ b/crates/sozo/ops/src/migration/utils.rs @@ -1,13 +1,20 @@ -use anyhow::{anyhow, Result}; +use std::collections::HashMap; + +use anyhow::{anyhow, Context, Result}; use camino::Utf8PathBuf; +use dojo_world::contracts::naming::get_namespace_from_tag; +use dojo_world::contracts::WorldContract; use dojo_world::manifest::{ AbstractManifestError, BaseManifest, DeploymentManifest, OverlayManifest, }; +use dojo_world::migration::world::WorldDiff; +use itertools::Itertools; use scarb_ui::Ui; -use starknet::accounts::ConnectedAccount; +use starknet::accounts::{Account, ConnectedAccount}; use starknet::core::types::Felt; use super::ui::MigrationUi; +use crate::auth::{get_resource_selector, ResourceType}; /// Loads: /// - `BaseManifest` from filesystem @@ -63,3 +70,63 @@ where Ok((local_manifest, remote_manifest)) } + +pub async fn generate_resource_map( + ui: &Ui, + world: &WorldContract, + diff: &WorldDiff, +) -> Result> +where + A: ConnectedAccount + Sync + Send, + ::SignError: 'static, +{ + let mut resource_map = HashMap::new(); + + for contract in diff.contracts.iter() { + let resource = ResourceType::Contract(contract.tag.clone()); + // we know the tag already contains the namespace + let default_namespace = get_namespace_from_tag(&contract.tag); + let selector = + get_resource_selector(ui, world, &resource, &default_namespace).await.with_context( + || format!("Failed to get resource selector for contract: {}", contract.tag), + )?; + + resource_map.insert(selector.to_hex_string(), resource); + } + + for model in diff.models.iter() { + let resource = ResourceType::Model(model.tag.clone()); + // we know the tag already contains the namespace + let default_namespace = get_namespace_from_tag(&model.tag); + let selector = get_resource_selector(ui, world, &resource, &default_namespace) + .await + .with_context(|| format!("Failed to get resource selector for model: {}", model.tag))?; + + resource_map.insert(selector.to_hex_string(), resource); + } + + // Collect all the namespaces from the contracts and models + let namespaces = { + let mut namespaces = + diff.models.iter().map(|m| get_namespace_from_tag(&m.tag)).collect::>(); + + namespaces.extend( + diff.contracts.iter().map(|c| get_namespace_from_tag(&c.tag)).collect::>(), + ); + + // remove duplicates + namespaces.into_iter().unique().collect::>() + }; + + for namespace in &namespaces { + let resource = ResourceType::Namespace(namespace.clone()); + let selector = + get_resource_selector(ui, world, &resource, "").await.with_context(|| { + format!("Failed to get resource selector for namespace: {}", namespace) + })?; + + resource_map.insert(selector.to_hex_string(), resource); + } + + Ok(resource_map) +} diff --git a/crates/sozo/ops/src/tests/auth.rs b/crates/sozo/ops/src/tests/auth.rs index ef467cdf05..b3a4006844 100644 --- a/crates/sozo/ops/src/tests/auth.rs +++ b/crates/sozo/ops/src/tests/auth.rs @@ -44,7 +44,7 @@ async fn auth_grant_writer_ok() { auth::grant_writer( &Ui::new(Verbosity::Normal, OutputFormat::Text), &world, - vec![moves_mc, position_mc], + &[moves_mc, position_mc], TxnConfig { wait: true, ..Default::default() }, DEFAULT_NAMESPACE, ) @@ -86,7 +86,7 @@ async fn auth_revoke_writer_ok() { auth::grant_writer( &Ui::new(Verbosity::Normal, OutputFormat::Text), &world, - vec![moves_mc.clone(), position_mc.clone()], + &[moves_mc.clone(), position_mc.clone()], TxnConfig { wait: true, ..Default::default() }, DEFAULT_NAMESPACE, ) @@ -100,7 +100,7 @@ async fn auth_revoke_writer_ok() { auth::revoke_writer( &Ui::new(Verbosity::Normal, OutputFormat::Text), &world, - vec![moves_mc, position_mc], + &[moves_mc, position_mc], TxnConfig { wait: true, ..Default::default() }, DEFAULT_NAMESPACE, ) @@ -141,7 +141,7 @@ async fn auth_grant_owner_ok() { auth::grant_owner( &Ui::new(Verbosity::Normal, OutputFormat::Text), &world, - vec![moves, position], + &[moves, position], TxnConfig { wait: true, ..Default::default() }, DEFAULT_NAMESPACE, ) @@ -181,7 +181,7 @@ async fn auth_revoke_owner_ok() { auth::grant_owner( &Ui::new(Verbosity::Normal, OutputFormat::Text), &world, - vec![moves.clone(), position.clone()], + &[moves.clone(), position.clone()], TxnConfig { wait: true, ..Default::default() }, DEFAULT_NAMESPACE, ) @@ -193,7 +193,7 @@ async fn auth_revoke_owner_ok() { auth::revoke_owner( &Ui::new(Verbosity::Normal, OutputFormat::Text), &world, - vec![moves, position], + &[moves, position], TxnConfig { wait: true, ..Default::default() }, DEFAULT_NAMESPACE, ) diff --git a/crates/sozo/ops/src/tests/call.rs b/crates/sozo/ops/src/tests/call.rs index 8265a04d42..a45162985d 100644 --- a/crates/sozo/ops/src/tests/call.rs +++ b/crates/sozo/ops/src/tests/call.rs @@ -124,7 +124,7 @@ async fn call_with_contract_address() { let contract_address = utils::get_contract_address::< SingleOwnerAccount, LocalWallet>, - >(&world, CONTRACT_TAG.to_string()) + >(&world, CONTRACT_TAG) .await .unwrap(); diff --git a/crates/sozo/ops/src/tests/migration.rs b/crates/sozo/ops/src/tests/migration.rs index ad061f59f7..8665b4866e 100644 --- a/crates/sozo/ops/src/tests/migration.rs +++ b/crates/sozo/ops/src/tests/migration.rs @@ -26,7 +26,9 @@ use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; use super::setup; -use crate::migration::{auto_authorize, execute_strategy, upload_metadata}; +use crate::migration::{ + auto_authorize, execute_strategy, find_authorization_diff, upload_metadata, +}; use crate::utils::get_contract_address_from_reader; #[tokio::test(flavor = "multi_thread")] @@ -58,7 +60,7 @@ async fn migrate_with_auto_mine() { let config = setup::load_config(); let ws = setup::setup_ws(&config); - let migration = setup::setup_migration(&config).unwrap(); + let (migration, _) = setup::setup_migration(&config).unwrap(); let sequencer = KatanaRunner::new().expect("Fail to start runner"); @@ -73,7 +75,7 @@ async fn migrate_with_block_time() { let config = setup::load_config(); let ws = setup::setup_ws(&config); - let migration = setup::setup_migration(&config).unwrap(); + let (migration, _) = setup::setup_migration(&config).unwrap(); let sequencer = KatanaRunner::new_with_config(KatanaRunnerConfig { block_time: Some(1000), @@ -93,7 +95,7 @@ async fn migrate_with_small_fee_multiplier_will_fail() { let config = setup::load_config(); let ws = setup::setup_ws(&config); - let migration = setup::setup_migration(&config).unwrap(); + let (migration, _) = setup::setup_migration(&config).unwrap(); let sequencer = KatanaRunner::new_with_config(KatanaRunnerConfig { disable_fee: true, @@ -136,7 +138,7 @@ async fn metadata_calculated_properly() { manifest.merge(overlay_manifest); } - let world = WorldDiff::compute(manifest, None); + let world = WorldDiff::compute(manifest, None, "dojo-test").unwrap(); let migration = prepare_for_migration( None, @@ -175,7 +177,7 @@ async fn migration_with_correct_calldata_second_time_work_as_expected() { ) .unwrap(); - let world = WorldDiff::compute(manifest.clone(), None); + let world = WorldDiff::compute(manifest.clone(), None, "dojo-test").unwrap(); let migration = prepare_for_migration( None, @@ -188,7 +190,7 @@ async fn migration_with_correct_calldata_second_time_work_as_expected() { let migration_output = execute_strategy(&ws, &migration, &account, TxnConfig::init_wait()).await.unwrap(); - // first time others will fail due to calldata error + // first time DojoContract named `others` will fail due to calldata error assert!(!migration_output.full); let world_address = migration_output.world_address; @@ -207,17 +209,16 @@ async fn migration_with_correct_calldata_second_time_work_as_expected() { } let default_namespace = get_default_namespace_from_ws(&ws).unwrap(); - let mut world = WorldDiff::compute(manifest, Some(remote_manifest)); - world.update_order(&default_namespace).expect("Failed to update order"); + let world = WorldDiff::compute(manifest, Some(remote_manifest), &default_namespace) + .expect("failed to update order"); - let mut migration = prepare_for_migration( + let migration = prepare_for_migration( Some(world_address), felt!("0x12345"), &Utf8Path::new(&target_dir).to_path_buf(), world, ) .unwrap(); - migration.resolve_variable(migration.world_address().unwrap()).expect("Failed to resolve"); let migration_output = execute_strategy(&ws, &migration, &account, TxnConfig::init_wait()).await.unwrap(); @@ -243,7 +244,7 @@ async fn migration_from_remote() { ) .unwrap(); - let world = WorldDiff::compute(manifest, None); + let world = WorldDiff::compute(manifest, None, "dojo-test").unwrap(); let migration = prepare_for_migration( None, @@ -262,7 +263,7 @@ async fn migration_from_remote() { let remote_manifest = DeploymentManifest::load_from_remote( JsonRpcClient::new(HttpTransport::new(sequencer.url())), - migration.world_address().unwrap(), + migration.world_address, ) .await .unwrap(); @@ -276,7 +277,7 @@ async fn migrate_with_metadata() { let config = setup::load_config(); let ws = setup::setup_ws(&config); - let migration = setup::setup_migration(&config).unwrap(); + let (migration, _) = setup::setup_migration(&config).unwrap(); let sequencer = KatanaRunner::new().expect("Fail to start runner"); @@ -349,8 +350,7 @@ async fn migrate_with_auto_authorize() { let config = setup::load_config(); let ws = setup::setup_ws(&config); - let mut migration = setup::setup_migration(&config).unwrap(); - migration.resolve_variable(migration.world_address().unwrap()).unwrap(); + let (migration, diff) = setup::setup_migration(&config).unwrap(); let manifest_base = config.manifest_path().parent().unwrap(); let mut manifest = @@ -372,12 +372,16 @@ async fn migrate_with_auto_authorize() { let output = execute_strategy(&ws, &migration, &account, txn_config).await.unwrap(); - let world_address = migration.world_address().expect("must be present"); + let world_address = migration.world_address; let world = WorldContract::new(world_address, account); let default_namespace = get_default_namespace_from_ws(&ws).unwrap(); - let res = - auto_authorize(&ws, &world, &txn_config, &manifest, &output, &default_namespace).await; + let (grant, revoke) = + find_authorization_diff(&config.ui(), &world, &diff, Some(&output), &default_namespace) + .await + .unwrap(); + + let res = auto_authorize(&ws, &world, &txn_config, &default_namespace, &grant, &revoke).await; assert!(res.is_ok()); let provider = sequencer.provider(); @@ -410,7 +414,7 @@ async fn migration_with_mismatching_world_address_and_seed() { let default_namespace = get_default_namespace_from_ws(&ws).unwrap(); - let strategy = prepare_migration_with_world_and_seed( + let (strategy, _) = prepare_migration_with_world_and_seed( base_dir, target_dir, Some(Felt::ONE), @@ -421,7 +425,7 @@ async fn migration_with_mismatching_world_address_and_seed() { // The strategy.world has it's address set with the seed directly, and not // from the world address provided by the user. - assert_ne!(strategy.world_address.unwrap(), strategy.world.unwrap().contract_address); + assert_ne!(strategy.world_address, strategy.world.unwrap().contract_address); } /// Get the hash from a IPFS URI diff --git a/crates/sozo/ops/src/tests/setup.rs b/crates/sozo/ops/src/tests/setup.rs index 06d8f8a779..7018cb47ec 100644 --- a/crates/sozo/ops/src/tests/setup.rs +++ b/crates/sozo/ops/src/tests/setup.rs @@ -4,6 +4,7 @@ use dojo_test_utils::migration::prepare_migration_with_world_and_seed; use dojo_world::contracts::world::WorldContract; use dojo_world::metadata::get_default_namespace_from_ws; use dojo_world::migration::strategy::MigrationStrategy; +use dojo_world::migration::world::WorldDiff; use dojo_world::migration::TxnConfig; use katana_runner::KatanaRunner; use scarb::compiler::Profile; @@ -49,7 +50,7 @@ pub fn setup_ws(config: &Config) -> Workspace<'_> { /// # Returns /// /// A [`MigrationStrategy`] to execute to migrate the full spawn-and-moves project. -pub fn setup_migration(config: &Config) -> Result { +pub fn setup_migration(config: &Config) -> Result<(MigrationStrategy, WorldDiff)> { let ws = setup_ws(config); let manifest_path = config.manifest_path(); @@ -83,9 +84,7 @@ pub async fn setup( let config = load_config(); let ws = setup_ws(&config); - let mut migration = setup_migration(&config)?; - let _ = - migration.resolve_variable(migration.world_address().expect("world address must exist")); + let (migration, _) = setup_migration(&config)?; let mut account = sequencer.account(0); account.set_block_id(BlockId::Tag(BlockTag::Pending)); @@ -97,6 +96,7 @@ pub async fn setup( TxnConfig { wait: true, ..Default::default() }, ) .await?; + // TODO: do we need to do authorization in setup? let world = WorldContract::new(output.world_address, account) .with_block(BlockId::Tag(BlockTag::Pending)); diff --git a/crates/sozo/ops/src/tests/utils.rs b/crates/sozo/ops/src/tests/utils.rs index 24585a9616..bd8c4c5568 100644 --- a/crates/sozo/ops/src/tests/utils.rs +++ b/crates/sozo/ops/src/tests/utils.rs @@ -15,8 +15,7 @@ async fn get_contract_address_from_world() { let world = setup::setup(&sequencer).await.unwrap(); - let contract_address = - utils::get_contract_address(&world, ACTION_CONTRACT_TAG.to_string()).await.unwrap(); + let contract_address = utils::get_contract_address(&world, ACTION_CONTRACT_TAG).await.unwrap(); assert!(contract_address != Felt::ZERO); } @@ -28,7 +27,7 @@ async fn get_contract_address_from_string() { let account = sequencer.account(0); let world = WorldContract::new(Felt::ZERO, account); - let contract_address = utils::get_contract_address(&world, "0x1234".to_string()).await.unwrap(); + let contract_address = utils::get_contract_address(&world, "0x1234").await.unwrap(); assert_eq!(contract_address, Felt::from_hex("0x1234").unwrap()); } diff --git a/crates/sozo/ops/src/utils.rs b/crates/sozo/ops/src/utils.rs index 0b6cdd70c4..83bdf458a2 100644 --- a/crates/sozo/ops/src/utils.rs +++ b/crates/sozo/ops/src/utils.rs @@ -23,14 +23,14 @@ use starknet::providers::Provider; /// A [`Felt`] with the address of the contract on success. pub async fn get_contract_address( world: &WorldContract, - tag_or_address: String, + tag_or_address: &str, ) -> Result { if tag_or_address.starts_with("0x") { - Felt::from_hex(&tag_or_address).map_err(anyhow::Error::from) + Felt::from_hex(tag_or_address).map_err(anyhow::Error::from) } else { let contract_class_hash = world.base().call().await?; Ok(starknet::core::utils::get_contract_address( - generate_salt(&get_name_from_tag(&tag_or_address)), + generate_salt(&get_name_from_tag(tag_or_address)), contract_class_hash.into(), &[], world.address, diff --git a/crates/torii/core/src/sql_test.rs b/crates/torii/core/src/sql_test.rs index f3f61bd2bf..963ba1731f 100644 --- a/crates/torii/core/src/sql_test.rs +++ b/crates/torii/core/src/sql_test.rs @@ -72,7 +72,7 @@ async fn test_load_from_remote() { &ws, None, sequencer.url().to_string(), - &account, + account, "dojo_examples", false, TxnConfig::init_wait(), @@ -82,6 +82,7 @@ async fn test_load_from_remote() { .unwrap() .unwrap(); + let account = sequencer.account(0); // spawn let tx = &account .execute_v1(vec![Call { @@ -204,7 +205,7 @@ async fn test_load_from_remote_del() { &ws, None, sequencer.url().to_string(), - &account, + account, "dojo_examples", false, TxnConfig::init_wait(), @@ -214,6 +215,7 @@ async fn test_load_from_remote_del() { .unwrap() .unwrap(); + let account = sequencer.account(0); // spawn account .execute_v1(vec![Call { @@ -316,7 +318,7 @@ async fn test_get_entity_keys() { &ws, None, sequencer.url().to_string(), - &account, + account, "dojo_examples", false, TxnConfig::init_wait(), @@ -326,6 +328,7 @@ async fn test_get_entity_keys() { .unwrap() .unwrap(); + let account = sequencer.account(0); // spawn account .execute_v1(vec![Call { diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index 9006a712ea..b2eb69adb8 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -285,7 +285,7 @@ pub async fn spinup_types_test() -> Result { &ws, None, sequencer.url().to_string(), - &account, + account, "types_test", false, TxnConfig::init_wait(), @@ -295,6 +295,7 @@ pub async fn spinup_types_test() -> Result { .unwrap() .unwrap(); + let account = sequencer.account(0); // Execute `create` and insert 11 records into storage let records_contract = migration_output .contracts diff --git a/crates/torii/grpc/src/server/tests/entities_test.rs b/crates/torii/grpc/src/server/tests/entities_test.rs index 196bca0bda..682ac2226f 100644 --- a/crates/torii/grpc/src/server/tests/entities_test.rs +++ b/crates/torii/grpc/src/server/tests/entities_test.rs @@ -49,20 +49,19 @@ async fn test_entities_queries() { let default_namespace = get_default_namespace_from_ws(&ws).unwrap(); - let mut migration = prepare_migration( + let migration = prepare_migration( config.manifest_path().parent().unwrap().into(), target_path, dojo_metadata.skip_migration, &default_namespace, ) .unwrap(); - migration.resolve_variable(migration.world_address().unwrap()).unwrap(); let sequencer = KatanaRunner::new().expect("Fail to start runner"); let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(sequencer.url()))); - let world = WorldContractReader::new(migration.world_address().unwrap(), &provider); + let world = WorldContractReader::new(migration.world_address, &provider); let account = sequencer.account(0); diff --git a/examples/spawn-and-move/Scarb.toml b/examples/spawn-and-move/Scarb.toml index 7740987830..c6cc3f9fd5 100644 --- a/examples/spawn-and-move/Scarb.toml +++ b/examples/spawn-and-move/Scarb.toml @@ -10,19 +10,16 @@ edition = "2024_07" sierra-replace-ids = true [dependencies] -dojo = { path = "../../crates/dojo-core" } armory = { path = "../game-lib/armory" } bestiary = { path = "../game-lib/bestiary" } +dojo = { path = "../../crates/dojo-core" } [[target.dojo]] -build-external-contracts = [ - "armory::Flatbow", - "bestiary::RiverSkale", -] +build-external-contracts = [ "armory::Flatbow", "bestiary::RiverSkale" ] [features] -default = ["dungeon"] -dungeon = [] +default = [ "dungeon" ] +dungeon = [ ] # `dev` profile diff --git a/examples/spawn-and-move/overlays/dev/actions.toml b/examples/spawn-and-move/overlays/dev/actions.toml index 31849b7894..72ac4c1e00 100644 --- a/examples/spawn-and-move/overlays/dev/actions.toml +++ b/examples/spawn-and-move/overlays/dev/actions.toml @@ -1,2 +1,2 @@ tag = "dojo_examples-actions" -writes = ["dojo_examples-Moves", "dojo_examples-Position"] +writes = [ "dojo_examples-Moves", "dojo_examples-Position" ]