diff --git a/crates/dojo-core/src/base.cairo b/crates/dojo-core/src/base.cairo new file mode 100644 index 0000000000..094a983742 --- /dev/null +++ b/crates/dojo-core/src/base.cairo @@ -0,0 +1,43 @@ +use starknet::{ClassHash, SyscallResult, SyscallResultTrait}; + +use dojo::world::IWorldDispatcher; + +#[starknet::interface] +trait IBase { + fn world(self: @T) -> IWorldDispatcher; +} + +#[starknet::contract] +mod base { + use starknet::{ClassHash, get_caller_address}; + + use dojo::upgradable::{IUpgradeable, UpgradeableTrait}; + use dojo::world::IWorldDispatcher; + + #[storage] + struct Storage { + world_dispatcher: IWorldDispatcher, + } + + #[constructor] + fn constructor(ref self: ContractState) { + self.world_dispatcher.write(IWorldDispatcher { contract_address: get_caller_address() }); + } + + #[external(v0)] + fn world(self: @ContractState) -> IWorldDispatcher { + self.world_dispatcher.read() + } + + #[external(v0)] + impl Upgradeable of IUpgradeable { + /// Upgrade contract implementation to new_class_hash + /// + /// # Arguments + /// + /// * `new_class_hash` - The new implementation class hahs. + fn upgrade(ref self: ContractState, new_class_hash: ClassHash) { + UpgradeableTrait::upgrade(new_class_hash); + } + } +} diff --git a/crates/dojo-core/src/base_test.cairo b/crates/dojo-core/src/base_test.cairo new file mode 100644 index 0000000000..691ad3bac6 --- /dev/null +++ b/crates/dojo-core/src/base_test.cairo @@ -0,0 +1,43 @@ +use option::OptionTrait; +use starknet::ClassHash; +use traits::TryInto; + +use dojo::base::base; +use dojo::upgradable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; +use dojo::test_utils::deploy_contract; + +#[starknet::contract] +mod contract_upgrade { + #[storage] + struct Storage {} + + #[starknet::interface] + trait IQuantumLeap { + fn plz_more_tps(self: @TState) -> felt252; + } + + #[constructor] + fn constructor(ref self: ContractState) {} + + #[external(v0)] + impl QuantumLeap of IQuantumLeap { + fn plz_more_tps(self: @ContractState) -> felt252 { + 'daddy' + } + } +} + +use contract_upgrade::{IQuantumLeapDispatcher, IQuantumLeapDispatcherTrait}; + +#[test] +#[available_gas(6000000)] +fn test_upgrade() { + let base_address = deploy_contract(base::TEST_CLASS_HASH, array![].span()); + let upgradable_dispatcher = IUpgradeableDispatcher { contract_address: base_address }; + + let new_class_hash: ClassHash = contract_upgrade::TEST_CLASS_HASH.try_into().unwrap(); + upgradable_dispatcher.upgrade(new_class_hash); + + let quantum_dispatcher = IQuantumLeapDispatcher { contract_address: base_address }; + assert(quantum_dispatcher.plz_more_tps() == 'daddy', 'quantum leap failed'); +} diff --git a/crates/dojo-core/src/lib.cairo b/crates/dojo-core/src/lib.cairo index 450842abc6..1fedb00bd7 100644 --- a/crates/dojo-core/src/lib.cairo +++ b/crates/dojo-core/src/lib.cairo @@ -1,3 +1,6 @@ +mod base; +#[cfg(test)] +mod base_test; mod database; #[cfg(test)] mod database_test; @@ -11,6 +14,7 @@ mod packing_test; mod world; #[cfg(test)] mod world_test; +mod upgradable; #[cfg(test)] mod test_utils; diff --git a/crates/dojo-core/src/test_utils.cairo b/crates/dojo-core/src/test_utils.cairo index eb3db0c220..d4abfa7f75 100644 --- a/crates/dojo-core/src/test_utils.cairo +++ b/crates/dojo-core/src/test_utils.cairo @@ -21,11 +21,11 @@ use dojo::world::{world, IWorldDispatcher, IWorldDispatcherTrait}; /// # Returns /// * address of contract deployed fn deploy_contract(class_hash: felt252, calldata: Span) -> ContractAddress { - let (system_contract, _) = starknet::deploy_syscall( + let (contract, _) = starknet::deploy_syscall( class_hash.try_into().unwrap(), 0, calldata, false ) .unwrap(); - system_contract + contract } /// Deploy classhash and passes in world address to constructor @@ -49,10 +49,8 @@ fn spawn_test_world(models: Array) -> IWorldDispatcher { ) .unwrap(); // deploy world - let mut world_constructor_calldata = array::ArrayTrait::new(); - world_constructor_calldata.append(executor_address.into()); let (world_address, _) = deploy_syscall( - world::TEST_CLASS_HASH.try_into().unwrap(), 0, world_constructor_calldata.span(), false + world::TEST_CLASS_HASH.try_into().unwrap(), 0, array![executor_address.into(), dojo::base::base::TEST_CLASS_HASH].span(), false ) .unwrap(); let world = IWorldDispatcher { contract_address: world_address }; diff --git a/crates/dojo-core/src/upgradable.cairo b/crates/dojo-core/src/upgradable.cairo new file mode 100644 index 0000000000..1a446770d8 --- /dev/null +++ b/crates/dojo-core/src/upgradable.cairo @@ -0,0 +1,19 @@ +use starknet::{ClassHash, SyscallResult, SyscallResultTrait}; +use zeroable::Zeroable; +use result::ResultTrait; + +#[starknet::interface] +trait IUpgradeable { + fn upgrade(ref self: T, new_class_hash: ClassHash); +} + +trait UpgradeableTrait { + fn upgrade(new_class_hash: ClassHash); +} + +impl UpgradeableTraitImpl of UpgradeableTrait { + fn upgrade(new_class_hash: ClassHash) { + assert(new_class_hash.is_non_zero(), 'class_hash cannot be zero'); + starknet::replace_class_syscall(new_class_hash).unwrap_syscall(); + } +} diff --git a/crates/dojo-core/src/world.cairo b/crates/dojo-core/src/world.cairo index fb792c34e3..d6bc989f15 100644 --- a/crates/dojo-core/src/world.cairo +++ b/crates/dojo-core/src/world.cairo @@ -6,6 +6,7 @@ use option::OptionTrait; trait IWorld { 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( @@ -29,6 +30,7 @@ trait IWorld { ) -> (Span, 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); fn is_owner(self: @T, address: ContractAddress, target: felt252) -> bool; fn grant_owner(ref self: T, address: ContractAddress, target: felt252); @@ -49,12 +51,13 @@ mod world { use starknet::{ get_caller_address, get_contract_address, get_tx_info, contract_address::ContractAddressIntoFelt252, ClassHash, Zeroable, ContractAddress, - syscalls::emit_event_syscall, SyscallResultTrait, SyscallResultTraitImpl + syscalls::{deploy_syscall, emit_event_syscall}, SyscallResult, SyscallResultTrait, SyscallResultTraitImpl }; use dojo::database; use dojo::database::index::WhereCondition; use dojo::executor::{IExecutorDispatcher, IExecutorDispatcherTrait}; + use dojo::upgradable::{IUpgradeableDispatcher, IUpgradeableDispatcherTrait}; use dojo::world::{IWorldDispatcher, IWorld}; @@ -101,18 +104,18 @@ mod world { #[storage] struct Storage { executor_dispatcher: IExecutorDispatcher, - models: LegacyMap::, + contract_base: ClassHash, nonce: usize, + models: LegacyMap::, owners: LegacyMap::<(felt252, ContractAddress), bool>, writers: LegacyMap::<(felt252, ContractAddress), bool>, - // Tracks the calling systems name for auth purposes. - call_stack_len: felt252, - call_stack: LegacyMap::, } #[constructor] - fn constructor(ref self: ContractState, executor: ContractAddress) { + fn constructor(ref self: ContractState, executor: ContractAddress, contract_base: ClassHash) { self.executor_dispatcher.write(IExecutorDispatcher { contract_address: executor }); + self.contract_base.write(contract_base); + self .owners .write( @@ -273,6 +276,23 @@ mod world { self.models.read(name) } + /// Deploys a contract associated with the world. + /// + /// # Arguments + /// + /// * `name` - The name of the contract. + /// * `class_hash` - The class_hash of the contract. + /// + /// # 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(); + let upgradable_dispatcher = IUpgradeableDispatcher { contract_address }; + upgradable_dispatcher.upgrade(class_hash); + contract_address + } + /// Issues an autoincremented id to the caller. /// /// # Returns @@ -408,6 +428,15 @@ mod world { fn executor(self: @ContractState) -> ContractAddress { self.executor_dispatcher.read().contract_address } + + /// Gets the base contract class hash. + /// + /// # Returns + /// + /// * `ContractAddress` - The address of the contract_base contract. + fn base(self: @ContractState) -> ClassHash { + self.contract_base.read() + } } /// Asserts that the current caller can write to the model. diff --git a/crates/dojo-lang/src/compiler.rs b/crates/dojo-lang/src/compiler.rs index c43e2f9645..83fe03ee23 100644 --- a/crates/dojo-lang/src/compiler.rs +++ b/crates/dojo-lang/src/compiler.rs @@ -161,6 +161,7 @@ fn find_project_contracts( pub fn collect_core_crate_ids(db: &RootDatabase) -> Vec { [ + ContractSelector("dojo::base::base".to_string()), ContractSelector("dojo::executor::executor".to_string()), ContractSelector("dojo::world::world".to_string()), ] diff --git a/crates/dojo-lang/src/manifest.rs b/crates/dojo-lang/src/manifest.rs index a02fc048bc..b1098b142e 100644 --- a/crates/dojo-lang/src/manifest.rs +++ b/crates/dojo-lang/src/manifest.rs @@ -7,7 +7,9 @@ use cairo_lang_semantic::db::SemanticGroup; use cairo_lang_starknet::abi; use cairo_lang_starknet::plugin::aux_data::StarkNetContractAuxData; use convert_case::{Case, Casing}; -use dojo_world::manifest::{Contract, EXECUTOR_CONTRACT_NAME, WORLD_CONTRACT_NAME}; +use dojo_world::manifest::{ + Class, Contract, BASE_CONTRACT_NAME, EXECUTOR_CONTRACT_NAME, WORLD_CONTRACT_NAME, +}; use serde::Serialize; use smol_str::SmolStr; use starknet::core::types::FieldElement; @@ -43,6 +45,15 @@ impl Manifest { ) ); }); + let (base, base_abi) = compiled_classes.get(BASE_CONTRACT_NAME).unwrap_or_else(|| { + panic!( + "{}", + format!( + "Contract `{}` not found. Did you include `dojo` as a dependency?", + BASE_CONTRACT_NAME + ) + ); + }); manifest.0.world = Contract { name: WORLD_CONTRACT_NAME.into(), @@ -50,6 +61,8 @@ impl Manifest { class_hash: *world, abi: world_abi.clone(), }; + manifest.0.base = + Class { name: BASE_CONTRACT_NAME.into(), class_hash: *base, abi: base_abi.clone() }; manifest.0.executor = Contract { name: EXECUTOR_CONTRACT_NAME.into(), address: None, @@ -137,7 +150,7 @@ impl Manifest { compiled_classes: &HashMap)>, ) { for name in &aux_data.contracts { - if "world" == name.as_str() || "executor" == name.as_str() { + if "world" == name.as_str() || "executor" == name.as_str() || "base" == name.as_str() { return; } diff --git a/crates/dojo-lang/src/manifest_test_data/manifest b/crates/dojo-lang/src/manifest_test_data/manifest index bb70482980..05ca3d3c88 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": "0xad01919d2f7edc172b74b0e258afb7bb44252582e02d5e245b88fcb488fb78", + "class_hash": "0x45539636cfa153f8ea0975102b04d35f455240c98c1dcf1eeb0013bef358d35", "abi": [ { "type": "impl", @@ -105,6 +105,26 @@ test_manifest_file "outputs": [], "state_mutability": "external" }, + { + "type": "function", + "name": "deploy_contract", + "inputs": [ + { + "name": "salt", + "type": "core::felt252" + }, + { + "name": "class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [ + { + "type": "core::starknet::contract_address::ContractAddress" + } + ], + "state_mutability": "view" + }, { "type": "function", "name": "uuid", @@ -247,6 +267,17 @@ test_manifest_file ], "state_mutability": "view" }, + { + "type": "function", + "name": "base", + "inputs": [], + "outputs": [ + { + "type": "core::starknet::class_hash::ClassHash" + } + ], + "state_mutability": "view" + }, { "type": "function", "name": "delete_entity", @@ -376,6 +407,10 @@ test_manifest_file { "name": "executor", "type": "core::starknet::contract_address::ContractAddress" + }, + { + "name": "contract_base", + "type": "core::starknet::class_hash::ClassHash" } ] }, @@ -544,6 +579,67 @@ test_manifest_file } ] }, + "base": { + "name": "base", + "class_hash": "0x7aec2b7d7064c1294a339cd90060331ff704ab573e4ee9a1b699be2215c11c9", + "abi": [ + { + "type": "impl", + "name": "Upgradeable", + "interface_name": "dojo::upgradable::IUpgradeable" + }, + { + "type": "interface", + "name": "dojo::upgradable::IUpgradeable", + "items": [ + { + "type": "function", + "name": "upgrade", + "inputs": [ + { + "name": "new_class_hash", + "type": "core::starknet::class_hash::ClassHash" + } + ], + "outputs": [], + "state_mutability": "external" + } + ] + }, + { + "type": "constructor", + "name": "constructor", + "inputs": [] + }, + { + "type": "struct", + "name": "dojo::world::IWorldDispatcher", + "members": [ + { + "name": "contract_address", + "type": "core::starknet::contract_address::ContractAddress" + } + ] + }, + { + "type": "function", + "name": "world", + "inputs": [], + "outputs": [ + { + "type": "dojo::world::IWorldDispatcher" + } + ], + "state_mutability": "view" + }, + { + "type": "event", + "name": "dojo::base::base::Event", + "kind": "enum", + "variants": [] + } + ] + }, "systems": [], "contracts": [ { diff --git a/crates/dojo-world/src/manifest.rs b/crates/dojo-world/src/manifest.rs index 529902b0fd..c489b4520c 100644 --- a/crates/dojo-world/src/manifest.rs +++ b/crates/dojo-world/src/manifest.rs @@ -23,6 +23,7 @@ mod test; pub const WORLD_CONTRACT_NAME: &str = "world"; pub const EXECUTOR_CONTRACT_NAME: &str = "executor"; +pub const BASE_CONTRACT_NAME: &str = "base"; #[derive(Error, Debug)] pub enum ManifestError { @@ -105,11 +106,21 @@ pub struct Contract { pub abi: Option, } +#[serde_as] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct Class { + pub name: SmolStr, + #[serde_as(as = "UfeHex")] + pub class_hash: FieldElement, + pub abi: Option, +} + #[serde_as] #[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] pub struct Manifest { pub world: Contract, pub executor: Contract, + pub base: Class, pub systems: Vec, pub contracts: Vec, pub models: Vec, @@ -166,6 +177,18 @@ impl Manifest { _ => ManifestError::Provider(err), })?; + let 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 + .map_err(ManifestError::Provider)?[0]; + let mut systems = vec![]; let mut models = vec![]; @@ -237,6 +260,11 @@ impl Manifest { class_hash: executor_class_hash, ..Default::default() }, + base: Class { + name: BASE_CONTRACT_NAME.into(), + class_hash: base_class_hash, + ..Default::default() + }, }) } } diff --git a/crates/dojo-world/src/migration/mod.rs b/crates/dojo-world/src/migration/mod.rs index 401ac5f821..f9a3b53411 100644 --- a/crates/dojo-world/src/migration/mod.rs +++ b/crates/dojo-world/src/migration/mod.rs @@ -9,10 +9,12 @@ use cairo_lang_starknet::contract_class::ContractClass; use starknet::accounts::{Account, AccountError, Call, ConnectedAccount, SingleOwnerAccount}; use starknet::core::types::contract::{CompiledClass, SierraClass}; use starknet::core::types::{ - BlockId, BlockTag, DeclareTransactionResult, FieldElement, FlattenedSierraClass, + BlockId, BlockTag, DeclareTransactionResult, FieldElement, FlattenedSierraClass, FunctionCall, InvokeTransactionResult, StarknetError, }; -use starknet::core::utils::{get_contract_address, CairoShortStringToFeltError}; +use starknet::core::utils::{ + get_contract_address, get_selector_from_name, CairoShortStringToFeltError, +}; use starknet::macros::{felt, selector}; use starknet::providers::{ MaybeUnknownErrorCode, Provider, ProviderError, StarknetErrorWithMessage, @@ -138,6 +140,74 @@ pub trait Declarable { #[cfg_attr(not(target_arch = "wasm32"), async_trait)] #[cfg_attr(target_arch = "wasm32", async_trait(?Send))] pub trait Deployable: Declarable + Sync { + async fn world_deploy( + &self, + world_address: FieldElement, + class_hash: FieldElement, + account: &SingleOwnerAccount, + txn_config: TxConfig, + ) -> Result< + DeployOutput, + MigrationError< as Account>::SignError,

::Error>, + > + where + P: Provider + Sync + Send, + S: Signer + Sync + Send, + { + let declare = match self.declare(account, txn_config).await { + Ok(res) => Some(res), + Err(MigrationError::ClassAlreadyDeclared) => None, + Err(e) => return Err(e), + }; + + let base_class_hash = account + .provider() + .call( + FunctionCall { + contract_address: world_address, + calldata: vec![], + entry_point_selector: get_selector_from_name("base").unwrap(), + }, + BlockId::Tag(BlockTag::Latest), + ) + .await + .map_err(MigrationError::Provider)?; + + let contract_address = + get_contract_address(self.salt(), base_class_hash[0], &[], world_address); + + match account + .provider() + .get_class_hash_at(BlockId::Tag(BlockTag::Pending), contract_address) + .await + { + Err(ProviderError::StarknetError(StarknetErrorWithMessage { + code: MaybeUnknownErrorCode::Known(StarknetError::ContractNotFound), + .. + })) => {} + + Ok(_) => return Err(MigrationError::ContractAlreadyDeployed(contract_address)), + Err(e) => return Err(MigrationError::Provider(e)), + } + + let mut txn = account.execute(vec![Call { + calldata: vec![self.salt(), class_hash], + selector: selector!("deploy_contract"), + to: world_address, + }]); + + if let TxConfig { fee_estimate_multiplier: Some(multiplier) } = txn_config { + txn = txn.fee_estimate_multiplier(multiplier); + } + + let InvokeTransactionResult { transaction_hash } = + txn.send().await.map_err(MigrationError::Migrator)?; + + TransactionWaiter::new(transaction_hash, account.provider()).await?; + + Ok(DeployOutput { transaction_hash, contract_address, declare }) + } + async fn deploy( &self, class_hash: FieldElement, @@ -154,7 +224,6 @@ 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), }; diff --git a/crates/dojo-world/src/migration/strategy.rs b/crates/dojo-world/src/migration/strategy.rs index eea4d9271a..5360d9088f 100644 --- a/crates/dojo-world/src/migration/strategy.rs +++ b/crates/dojo-world/src/migration/strategy.rs @@ -5,8 +5,8 @@ use std::path::{Path, PathBuf}; use anyhow::{anyhow, Context, Result}; use convert_case::{Case, Casing}; use starknet::core::types::FieldElement; -use starknet::core::utils::get_contract_address; -use starknet_crypto::poseidon_hash_single; +use starknet::core::utils::{cairo_short_string_to_felt, get_contract_address}; +use starknet_crypto::{poseidon_hash_many, poseidon_hash_single}; use super::class::{ClassDiff, ClassMigration}; use super::contract::{ContractDiff, ContractMigration}; @@ -26,6 +26,7 @@ pub struct MigrationStrategy { pub world_address: Option, pub world: Option, pub executor: Option, + pub base: Option, pub contracts: Vec, pub models: Vec, } @@ -111,6 +112,7 @@ where 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 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())?; @@ -129,12 +131,12 @@ where world.contract_address = get_contract_address( salt, diff.world.local, - &[executor.as_ref().unwrap().contract_address], + &[executor.as_ref().unwrap().contract_address, base.as_ref().unwrap().diff.local], FieldElement::ZERO, ); } - Ok(MigrationStrategy { world_address, world, executor, contracts, models }) + Ok(MigrationStrategy { world_address, world, executor, base, contracts, models }) } fn evaluate_models_to_migrate( @@ -145,20 +147,31 @@ fn evaluate_models_to_migrate( let mut comps_to_migrate = vec![]; for c in models { - match c.remote { - Some(remote) if remote == c.local && !world_contract_will_migrate => continue, - _ => { - let path = - find_artifact_path(c.name.to_case(Case::Snake).as_str(), artifact_paths)?; - comps_to_migrate - .push(ClassMigration { diff: c.clone(), artifact_path: path.clone() }); - } + if let Ok(Some(c)) = + evaluate_class_to_migrate(c, artifact_paths, world_contract_will_migrate) + { + comps_to_migrate.push(c); } } Ok(comps_to_migrate) } +fn evaluate_class_to_migrate( + class: &ClassDiff, + artifact_paths: &HashMap, + world_contract_will_migrate: bool, +) -> Result> { + match class.remote { + Some(remote) if remote == class.local && !world_contract_will_migrate => Ok(None), + _ => { + let path = + find_artifact_path(class.name.to_case(Case::Snake).as_str(), artifact_paths)?; + Ok(Some(ClassMigration { diff: class.clone(), artifact_path: path.clone() })) + } + } +} + fn evaluate_contracts_to_migrate( contracts: &[ContractDiff], artifact_paths: &HashMap, @@ -175,6 +188,17 @@ fn evaluate_contracts_to_migrate( comps_to_migrate.push(ContractMigration { diff: c.clone(), artifact_path: path.clone(), + salt: poseidon_hash_many( + &c.name + .chars() + .collect::>() + .chunks(31) + .map(|chunk| { + let s: String = chunk.iter().collect(); + cairo_short_string_to_felt(&s).unwrap() + }) + .collect::>(), + ), ..Default::default() }); } diff --git a/crates/dojo-world/src/migration/world.rs b/crates/dojo-world/src/migration/world.rs index 52fd359b5d..06b1c6a663 100644 --- a/crates/dojo-world/src/migration/world.rs +++ b/crates/dojo-world/src/migration/world.rs @@ -3,7 +3,7 @@ use std::fmt::Display; use super::class::ClassDiff; use super::contract::ContractDiff; use super::StateDiff; -use crate::manifest::{Manifest, EXECUTOR_CONTRACT_NAME, WORLD_CONTRACT_NAME}; +use crate::manifest::{Manifest, BASE_CONTRACT_NAME, EXECUTOR_CONTRACT_NAME, WORLD_CONTRACT_NAME}; #[cfg(test)] #[path = "world_test.rs"] @@ -14,6 +14,7 @@ mod tests; pub struct WorldDiff { pub world: ContractDiff, pub executor: ContractDiff, + pub base: ClassDiff, pub contracts: Vec, pub models: Vec, pub systems: Vec, @@ -65,13 +66,19 @@ impl WorldDiff { 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 world = ContractDiff { name: WORLD_CONTRACT_NAME.into(), local: local.world.class_hash, remote: remote.map(|m| m.world.class_hash), }; - WorldDiff { world, executor, systems, contracts, models } + WorldDiff { world, executor, base, systems, contracts, models } } pub fn count_diffs(&self) -> usize { diff --git a/crates/sozo/src/ops/migration/mod.rs b/crates/sozo/src/ops/migration/mod.rs index 2b53ebd9df..3571825d21 100644 --- a/crates/sozo/src/ops/migration/mod.rs +++ b/crates/sozo/src/ops/migration/mod.rs @@ -7,12 +7,11 @@ use dojo_world::migration::contract::ContractMigration; use dojo_world::migration::strategy::{prepare_for_migration, MigrationStrategy}; use dojo_world::migration::world::WorldDiff; use dojo_world::migration::{ - read_class, Declarable, DeployOutput, Deployable, MigrationError, RegisterOutput, StateDiff, + Declarable, DeployOutput, Deployable, MigrationError, RegisterOutput, StateDiff, }; use dojo_world::utils::TransactionWaiter; use scarb_ui::Ui; use starknet::accounts::{Account, ConnectedAccount, SingleOwnerAccount}; -use starknet::core::types::contract::AbiEntry; use starknet::core::types::{ BlockId, BlockTag, FieldElement, InvokeTransactionResult, StarknetError, }; @@ -272,10 +271,34 @@ where None => {} }; + match &strategy.base { + Some(base) => { + ui.print_header("# Base Contract"); + + match base + .declare(migrator, txn_config.clone().map(|c| c.into()).unwrap_or_default()) + .await + { + Ok(res) => { + ui.print_sub(format!("Class Hash: {:#x}", res.class_hash)); + } + Err(MigrationError::ClassAlreadyDeclared) => { + ui.print_sub(format!("Already declared: {:#x}", base.diff.local)); + } + Err(e) => return Err(e.into()), + }; + } + None => {} + }; + match &strategy.world { Some(world) => { ui.print_header("# World"); - let calldata = vec![strategy.executor.as_ref().unwrap().contract_address]; + + let calldata = vec![ + strategy.executor.as_ref().unwrap().contract_address, + strategy.base.as_ref().unwrap().diff.local, + ]; deploy_contract(world, "world", calldata, migrator, ui, &txn_config).await?; ui.print_sub(format!("Contract address: {:#x}", world.contract_address)); @@ -371,7 +394,7 @@ where // Continue if model is already declared Err(MigrationError::ClassAlreadyDeclared) => { - ui.print_sub("Already declared"); + ui.print_sub(format!("Already declared: {:#x}", c.diff.local)); continue; } Err(e) => bail!("Failed to declare model {}: {e}", c.diff.name), @@ -414,35 +437,38 @@ where let mut deploy_output = vec![]; - for contract in strategy.contracts.iter() { - let mut constructor_calldata = vec![]; - let class = read_class(contract.artifact_path())?; - for entry in class.abi { - if let AbiEntry::Constructor(constructor) = entry { - if !constructor.inputs.is_empty() - && constructor.inputs[0].r#type == "dojo::world::IWorldDispatcher" - { - let world_address = strategy.world_address()?; - constructor_calldata.push(world_address); - break; - } - } - } + let world_address = strategy.world_address()?; + for contract in strategy.contracts.iter() { let name = &contract.diff.name; ui.print(italic_message(name).to_string()); - match deploy_contract(contract, name, constructor_calldata, migrator, ui, &txn_config) - .await? + match contract + .world_deploy( + world_address, + contract.diff.local, + migrator, + txn_config.clone().map(|c| c.into()).unwrap_or_default(), + ) + .await { - ContractDeploymentOutput::Output(output) => { + Ok(output) => { + if let Some(declare) = output.clone().declare { + ui.print_hidden_sub(format!( + "Declare transaction: {:#x}", + declare.transaction_hash + )); + } + + ui.print_hidden_sub(format!("Deploy transaction: {:#x}", output.transaction_hash)); ui.print_sub(format!("Contract address: {:#x}", output.contract_address)); ui.print_hidden_sub(format!("deploy transaction: {:#x}", output.transaction_hash)); deploy_output.push(Some(output)); } - ContractDeploymentOutput::AlreadyDeployed(contract_address) => { + Err(MigrationError::ContractAlreadyDeployed(contract_address)) => { ui.print_sub(format!("Already deployed: {:#x}", contract_address)); deploy_output.push(None); } + Err(e) => return Err(anyhow!("Failed to migrate {}: {:?}", name, e)), } } diff --git a/crates/torii/client/src/contract/world.rs b/crates/torii/client/src/contract/world.rs index 135b3618ac..aa7ee4c91f 100644 --- a/crates/torii/client/src/contract/world.rs +++ b/crates/torii/client/src/contract/world.rs @@ -93,6 +93,22 @@ impl<'a, A: ConnectedAccount + Sync> WorldContract<'a, A> { self.account.execute(calls).send().await } + pub async fn deploy_contract( + &self, + salt: &FieldElement, + class_hash: &FieldElement, + ) -> Result::Error>> + { + self.account + .execute(vec![Call { + to: self.address, + selector: get_selector_from_name("deploy_contract").unwrap(), + calldata: vec![*salt, *class_hash], + }]) + .send() + .await + } + pub async fn executor( &self, block_id: BlockId, @@ -100,13 +116,11 @@ impl<'a, A: ConnectedAccount + Sync> WorldContract<'a, A> { self.reader.executor(block_id).await } - pub async fn call( + pub async fn base( &self, - system: &str, - calldata: Vec, block_id: BlockId, - ) -> Result, ContractReaderError<::Error>> { - self.reader.call(system, calldata, block_id).await + ) -> Result::Error>> { + self.reader.base(block_id).await } pub async fn model( @@ -207,44 +221,40 @@ impl<'a, P: Provider + Sync> WorldContractReader<'a, P> { Ok(res[0]) } - pub async fn executor_call( + pub async fn base( &self, - class_hash: FieldElement, - mut calldata: Vec, block_id: BlockId, - ) -> Result, ContractReaderError> { - calldata.insert(0, class_hash); - - self.provider + ) -> Result> { + let res = self + .provider .call( FunctionCall { - contract_address: self.executor(block_id).await.unwrap(), - calldata, - entry_point_selector: get_selector_from_name("call").unwrap(), + contract_address: self.address, + calldata: vec![], + entry_point_selector: get_selector_from_name("base").unwrap(), }, block_id, ) .await - .map_err(ContractReaderError::ProviderError) + .map_err(ContractReaderError::ProviderError)?; + + Ok(res[0]) } - pub async fn call( + pub async fn executor_call( &self, - system: &str, + class_hash: FieldElement, mut calldata: Vec, block_id: BlockId, ) -> Result, ContractReaderError> { - calldata.insert( - 0, - cairo_short_string_to_felt(system) - .map_err(ContractReaderError::CairoShortStringToFeltError)?, - ); + calldata.insert(0, class_hash); + self.provider .call( FunctionCall { - contract_address: self.address, + contract_address: self.executor(block_id).await.unwrap(), calldata, - entry_point_selector: get_selector_from_name("execute").unwrap(), + entry_point_selector: get_selector_from_name("call").unwrap(), }, block_id, ) diff --git a/crates/torii/client/src/contract/world_test.rs b/crates/torii/client/src/contract/world_test.rs index 450efaffc0..becf7c836d 100644 --- a/crates/torii/client/src/contract/world_test.rs +++ b/crates/torii/client/src/contract/world_test.rs @@ -54,6 +54,9 @@ pub async fn deploy_world( .unwrap() .contract_address; + let base_class_hash = + strategy.base.unwrap().declare(&account, Default::default()).await.unwrap().class_hash; + // wait for the tx to be mined tokio::time::sleep(Duration::from_millis(250)).await; @@ -62,7 +65,7 @@ pub async fn deploy_world( .unwrap() .deploy( manifest.clone().world.class_hash, - vec![executor_address], + vec![executor_address, base_class_hash], &account, Default::default(), )