diff --git a/crates/dojo-lang/src/compiler.rs b/crates/dojo-lang/src/compiler.rs index a28f26c34f..adb49738a8 100644 --- a/crates/dojo-lang/src/compiler.rs +++ b/crates/dojo-lang/src/compiler.rs @@ -234,7 +234,7 @@ fn update_manifest( &manifest_dir, &mut Manifest::new( // abi path will be written by `write_manifest` - Class { class_hash: *hash, abi: None }, + Class { class_hash: *hash, abi: None, original_class_hash: *hash }, WORLD_CONTRACT_NAME.into(), ), abi, @@ -245,7 +245,10 @@ fn update_manifest( &relative_manifests_dir, &relative_abis_dir, &manifest_dir, - &mut Manifest::new(Class { class_hash: *hash, abi: None }, BASE_CONTRACT_NAME.into()), + &mut Manifest::new( + Class { class_hash: *hash, abi: None, original_class_hash: *hash }, + BASE_CONTRACT_NAME.into(), + ), &None, )?; @@ -352,7 +355,12 @@ fn get_dojo_model_artifacts( model_full_name.clone(), ( Manifest::new( - DojoModel { class_hash, abi: None, members: model.members.clone() }, + DojoModel { + class_hash, + abi: None, + members: model.members.clone(), + original_class_hash: class_hash, + }, model_full_name.into(), ), abi, @@ -423,7 +431,7 @@ fn get_dojo_contract_artifacts( writes, reads, class_hash: *class_hash, - abi: None, + original_class_hash: *class_hash, ..Default::default() }, module_name.clone(), diff --git a/crates/dojo-world/src/contracts/world_test.rs b/crates/dojo-world/src/contracts/world_test.rs index e9cfb18e16..f7aadbecc4 100644 --- a/crates/dojo-world/src/contracts/world_test.rs +++ b/crates/dojo-world/src/contracts/world_test.rs @@ -91,7 +91,13 @@ pub async fn deploy_world( for contract in strategy.contracts { let declare_res = contract.declare(&account, Default::default()).await.unwrap(); contract - .world_deploy(world_address, declare_res.class_hash, &account, Default::default()) + .deploy_dojo_contract( + world_address, + declare_res.class_hash, + base_class_hash, + &account, + Default::default(), + ) .await .unwrap(); } diff --git a/crates/dojo-world/src/manifest_test.rs b/crates/dojo-world/src/manifest/manifest_test.rs similarity index 94% rename from crates/dojo-world/src/manifest_test.rs rename to crates/dojo-world/src/manifest/manifest_test.rs index 6c3a0a3ae9..6674127de7 100644 --- a/crates/dojo-world/src/manifest_test.rs +++ b/crates/dojo-world/src/manifest/manifest_test.rs @@ -430,3 +430,29 @@ fn test_abi_format_to_embed() -> Result<(), Box> { Ok(()) } + +#[test] +fn test_abi_format_to_path() { + let embedded = AbiFormat::Embed(vec![]); + assert!(embedded.to_path().is_none()); + + let path = AbiFormat::Path(Utf8PathBuf::from("/tmp")); + assert!(path.to_path().is_some()); +} + +#[test] +fn test_abi_format_load_abi_string() -> Result<(), Box> { + let temp_dir = tempfile::tempdir()?; + let temp_path = temp_dir.path().join("abi.json"); + let mut temp_file = std::fs::File::create(&temp_path)?; + + write!(temp_file, "[]")?; + + let path = AbiFormat::Path(Utf8PathBuf::from_path_buf(temp_path.clone()).unwrap()); + assert_eq!(path.load_abi_string(&Utf8PathBuf::new()).unwrap(), "[]"); + + let embedded = AbiFormat::Embed(vec![]); + assert_eq!(embedded.load_abi_string(&Utf8PathBuf::new()).unwrap(), "[]"); + + Ok(()) +} diff --git a/crates/dojo-world/src/manifest.rs b/crates/dojo-world/src/manifest/mod.rs similarity index 70% rename from crates/dojo-world/src/manifest.rs rename to crates/dojo-world/src/manifest/mod.rs index fcef14ab1c..c99a07e0ca 100644 --- a/crates/dojo-world/src/manifest.rs +++ b/crates/dojo-world/src/manifest/mod.rs @@ -5,11 +5,7 @@ use anyhow::Result; use cainome::cairo_serde::Error as CainomeError; use camino::Utf8PathBuf; use serde::de::DeserializeOwned; -use serde::{Deserialize, Serialize}; -use serde_with::serde_as; use smol_str::SmolStr; -use starknet::core::serde::unsigned_field_element::UfeHex; -use starknet::core::types::contract::AbiEntry; use starknet::core::types::{ BlockId, BlockTag, EmittedEvent, EventFilter, FieldElement, FunctionCall, StarknetError, }; @@ -30,6 +26,14 @@ use crate::contracts::WorldContractReader; #[path = "manifest_test.rs"] mod test; +mod types; + +pub use types::{ + AbiFormat, BaseManifest, Class, ComputedValueEntrypoint, Contract, DeploymentManifest, + DojoContract, DojoModel, Manifest, ManifestMethods, Member, OverlayClass, OverlayContract, + OverlayDojoContract, OverlayDojoModel, OverlayManifest, +}; + pub const WORLD_CONTRACT_NAME: &str = "dojo::world::world"; pub const BASE_CONTRACT_NAME: &str = "dojo::base::base"; pub const RESOURCE_METADATA_CONTRACT_NAME: &str = "dojo::resource_metadata::resource_metadata"; @@ -61,206 +65,13 @@ pub enum AbstractManifestError { Json(#[from] serde_json::Error), } -/// Represents a model member. -#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] -pub struct Member { - /// Name of the member. - pub name: String, - /// Type of the member. - #[serde(rename = "type")] - pub ty: String, - pub key: bool, -} - -impl From for Member { - fn from(m: dojo_types::schema::Member) -> Self { - Self { name: m.name, ty: m.ty.name(), key: m.key } - } -} - -/// Represents a declaration of a model. -#[serde_as] -#[derive(Clone, Default, Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -#[serde(tag = "kind")] -pub struct DojoModel { - pub members: Vec, - #[serde_as(as = "UfeHex")] - pub class_hash: FieldElement, - pub abi: Option, -} - -/// System input ABI. -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct Input { - pub name: String, - #[serde(rename = "type")] - pub ty: String, -} - -/// System Output ABI. -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct Output { - #[serde(rename = "type")] - pub ty: String, -} - -#[serde_as] -#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] -pub struct ComputedValueEntrypoint { - // Name of the contract containing the entrypoint - pub contract: SmolStr, - // Name of entrypoint to get computed value - pub entrypoint: SmolStr, - // Component to compute for - pub model: Option, -} - -/// Format of the ABI into the manifest. -#[serde_as] -#[derive(Clone, Debug, Serialize, Deserialize)] -#[serde(untagged)] -pub enum AbiFormat { - /// Only a relative path to the ABI file is stored. - Path(Utf8PathBuf), - /// The full ABI is embedded. - Embed(Vec), -} - -impl AbiFormat { - /// Get the [`Utf8PathBuf`] if the ABI is stored as a path. - pub fn to_path(&self) -> Option<&Utf8PathBuf> { - match self { - AbiFormat::Path(p) => Some(p), - AbiFormat::Embed(_) => None, - } - } - - /// Loads an ABI from the path or embedded entries. - /// - /// # Arguments - /// - /// * `root_dir` - The root directory of the ABI file. - pub fn load_abi_string(&self, root_dir: &Utf8PathBuf) -> Result { - match self { - AbiFormat::Path(abi_path) => Ok(fs::read_to_string(root_dir.join(abi_path))?), - AbiFormat::Embed(abi) => Ok(serde_json::to_string(&abi)?), - } - } - - /// Convert to embed variant. - /// - /// # Arguments - /// - /// * `root_dir` - The root directory for the abi file resolution. - pub fn to_embed(&self, root_dir: &Utf8PathBuf) -> Result { - if let AbiFormat::Path(abi_path) = self { - let mut abi_file = std::fs::File::open(root_dir.join(abi_path))?; - Ok(serde_json::from_reader(&mut abi_file)?) - } else { - Ok(self.clone()) - } - } -} - -#[cfg(test)] -impl PartialEq for AbiFormat { - fn eq(&self, other: &Self) -> bool { - match (self, other) { - (AbiFormat::Path(p1), AbiFormat::Path(p2)) => p1 == p2, - (AbiFormat::Embed(e1), AbiFormat::Embed(e2)) => { - // Currently, [`AbiEntry`] does not implement [`PartialEq`] so we cannot compare - // them directly. - let e1_json = serde_json::to_string(e1).expect("valid JSON from ABI"); - let e2_json = serde_json::to_string(e2).expect("valid JSON from ABI"); - e1_json == e2_json - } - _ => false, - } - } -} - -#[serde_as] -#[derive(Clone, Default, Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -#[serde(tag = "kind")] -pub struct DojoContract { - #[serde_as(as = "Option")] - pub address: Option, - #[serde_as(as = "UfeHex")] - pub class_hash: FieldElement, - pub abi: Option, - pub reads: Vec, - pub writes: Vec, - pub computed: Vec, -} - -#[serde_as] -#[derive(Clone, Default, Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -pub struct OverlayDojoContract { - pub name: SmolStr, - pub reads: Option>, - pub writes: Option>, -} - -#[serde_as] -#[derive(Clone, Default, Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -pub struct OverlayDojoModel {} - -#[serde_as] -#[derive(Clone, Default, Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -pub struct OverlayContract {} - -#[serde_as] -#[derive(Clone, Default, Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -pub struct OverlayClass {} - -#[serde_as] -#[derive(Clone, Default, Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -#[serde(tag = "kind")] -pub struct Class { - #[serde_as(as = "UfeHex")] - pub class_hash: FieldElement, - pub abi: Option, -} - -#[serde_as] -#[derive(Clone, Default, Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -#[serde(tag = "kind")] -pub struct Contract { - #[serde_as(as = "UfeHex")] - pub class_hash: FieldElement, - pub abi: Option, - #[serde_as(as = "Option")] - pub address: Option, - #[serde_as(as = "Option")] - pub transaction_hash: Option, - pub block_number: Option, - // used by World contract - pub seed: Option, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -pub struct BaseManifest { - pub world: Manifest, - pub base: Manifest, - pub contracts: Vec>, - pub models: Vec>, -} - impl From> for Manifest { fn from(value: Manifest) -> Self { Manifest::new( Contract { class_hash: value.inner.class_hash, abi: value.inner.abi, + original_class_hash: value.inner.original_class_hash, ..Default::default() }, value.name, @@ -279,54 +90,6 @@ impl From for DeploymentManifest { } } -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -pub struct DeploymentManifest { - pub world: Manifest, - pub base: Manifest, - pub contracts: Vec>, - pub models: Vec>, -} - -#[derive(Clone, Debug, Serialize, Deserialize)] -#[cfg_attr(test, derive(PartialEq))] -pub struct OverlayManifest { - pub contracts: Vec, -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -#[cfg_attr(test, derive(PartialEq))] -pub struct Manifest -where - T: ManifestMethods, -{ - #[serde(flatten)] - pub inner: T, - pub name: SmolStr, -} - -impl Manifest -where - T: ManifestMethods, -{ - pub fn new(inner: T, name: SmolStr) -> Self { - Self { inner, name } - } -} - -pub trait ManifestMethods { - type OverlayType; - fn abi(&self) -> Option<&AbiFormat>; - fn set_abi(&mut self, abi: Option); - fn class_hash(&self) -> &FieldElement; - fn set_class_hash(&mut self, class_hash: FieldElement); - - /// This method is called when during compilation base manifest file already exists. - /// Manifest generated during compilation won't contains properties manually updated by users - /// (like calldata) so this method should override those fields - fn merge(&mut self, old: Self::OverlayType); -} - impl BaseManifest { /// Load the manifest from a file at the given path. pub fn load_from_path(path: &Utf8PathBuf) -> Result { @@ -355,15 +118,38 @@ impl BaseManifest { .inner .merge(contract); } + + if let Some(overlay_world) = overlay.world { + self.world.inner.merge(overlay_world); + } + if let Some(overlay_base) = overlay.base { + self.base.inner.merge(overlay_base); + } } } impl OverlayManifest { pub fn load_from_path(path: &Utf8PathBuf) -> Result { + let mut world: Option = None; + let world_path = path.join("world.toml"); + if world_path.exists() { + world = Some(toml::from_str(&fs::read_to_string(world_path)?)?); + } + let mut base: Option = None; + let base_path = path.join("base.toml"); + if base_path.exists() { + base = Some(toml::from_str(&fs::read_to_string(path.join("base.toml"))?)?); + } + let contract_dir = path.join("contracts"); - let contracts = overlay_elements_from_path::(&contract_dir)?; - Ok(Self { contracts }) + let contracts = if contract_dir.exists() { + overlay_elements_from_path::(&contract_dir)? + } else { + vec![] + }; + + Ok(Self { world, base, contracts }) } } @@ -378,6 +164,13 @@ impl DeploymentManifest { self.world.inner.transaction_hash = previous.world.inner.transaction_hash; self.world.inner.block_number = previous.world.inner.block_number; self.world.inner.seed = previous.world.inner.seed; + + self.contracts.iter_mut().for_each(|contract| { + let previous_contract = previous.contracts.iter().find(|c| c.name == contract.name); + if let Some(previous_contract) = previous_contract { + contract.inner.base_class_hash = previous_contract.inner.base_class_hash; + } + }); } pub fn write_to_path_toml(&self, path: &Utf8PathBuf) -> Result<()> { @@ -442,6 +235,7 @@ impl DeploymentManifest { let world = WorldContractReader::new(world_address, provider); let base_class_hash = world.base().block_id(BLOCK_ID).call().await?; + let base_class_hash = base_class_hash.into(); let (models, contracts) = get_remote_models_and_contracts(world_address, &world.provider()).await?; @@ -458,7 +252,11 @@ impl DeploymentManifest { WORLD_CONTRACT_NAME.into(), ), base: Manifest::new( - Class { class_hash: base_class_hash.into(), abi: None }, + Class { + class_hash: base_class_hash, + abi: None, + original_class_hash: base_class_hash, + }, BASE_CONTRACT_NAME.into(), ), }) @@ -752,7 +550,14 @@ impl ManifestMethods for DojoContract { self.class_hash = class_hash; } + fn original_class_hash(&self) -> &FieldElement { + self.original_class_hash.as_ref() + } + fn merge(&mut self, old: Self::OverlayType) { + if let Some(class_hash) = old.original_class_hash { + self.original_class_hash = class_hash; + } if let Some(reads) = old.reads { self.reads = reads; } @@ -781,7 +586,15 @@ impl ManifestMethods for DojoModel { self.class_hash = class_hash; } - fn merge(&mut self, _: Self::OverlayType) {} + fn original_class_hash(&self) -> &FieldElement { + self.original_class_hash.as_ref() + } + + fn merge(&mut self, old: Self::OverlayType) { + if let Some(class_hash) = old.original_class_hash { + self.original_class_hash = class_hash; + } + } } impl ManifestMethods for Contract { @@ -803,7 +616,15 @@ impl ManifestMethods for Contract { self.class_hash = class_hash; } - fn merge(&mut self, _: Self::OverlayType) {} + fn original_class_hash(&self) -> &FieldElement { + self.original_class_hash.as_ref() + } + + fn merge(&mut self, old: Self::OverlayType) { + if let Some(class_hash) = old.original_class_hash { + self.original_class_hash = class_hash; + } + } } impl ManifestMethods for Class { @@ -825,5 +646,13 @@ impl ManifestMethods for Class { self.class_hash = class_hash; } - fn merge(&mut self, _: Self::OverlayType) {} + fn original_class_hash(&self) -> &FieldElement { + self.original_class_hash.as_ref() + } + + fn merge(&mut self, old: Self::OverlayType) { + if let Some(class_hash) = old.original_class_hash { + self.original_class_hash = class_hash; + } + } } diff --git a/crates/dojo-world/src/manifest/types.rs b/crates/dojo-world/src/manifest/types.rs new file mode 100644 index 0000000000..fe8c09dc5e --- /dev/null +++ b/crates/dojo-world/src/manifest/types.rs @@ -0,0 +1,288 @@ +use std::fs; + +use camino::Utf8PathBuf; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use smol_str::SmolStr; +use starknet::core::serde::unsigned_field_element::UfeHex; +use starknet::core::types::contract::AbiEntry; +use starknet_crypto::FieldElement; + +use crate::manifest::AbstractManifestError; + +// Collection of different types of `Manifest`'s which are used by dojo compiler/sozo +// For example: +// - `BaseManifest` is generated by the compiler and wrote to `manifests/base` folder of project +// - `DeploymentManifest` is generated by sozo which represents the future onchain state after a +// successful migration +// - `OverlayManifest` is used by sozo to override values of specific manifest of `BaseManifest` +// thats generated by compiler + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct BaseManifest { + pub world: Manifest, + pub base: Manifest, + pub contracts: Vec>, + pub models: Vec>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct DeploymentManifest { + pub world: Manifest, + pub base: Manifest, + pub contracts: Vec>, + pub models: Vec>, +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct OverlayManifest { + pub world: Option, + pub base: Option, + pub contracts: Vec, +} + +#[derive(Clone, Serialize, Deserialize, Debug)] +#[cfg_attr(test, derive(PartialEq))] +pub struct Manifest +where + T: ManifestMethods, +{ + #[serde(flatten)] + pub inner: T, + pub name: SmolStr, +} + +// Utility methods thats needs to be implemented by manifest types +pub trait ManifestMethods { + type OverlayType; + fn abi(&self) -> Option<&AbiFormat>; + fn set_abi(&mut self, abi: Option); + fn class_hash(&self) -> &FieldElement; + fn set_class_hash(&mut self, class_hash: FieldElement); + fn original_class_hash(&self) -> &FieldElement; + + /// This method is called when during compilation base manifest file already exists. + /// Manifest generated during compilation won't contains properties manually updated by users + /// (like calldata) so this method should override those fields + fn merge(&mut self, old: Self::OverlayType); +} + +impl Manifest +where + T: ManifestMethods, +{ + pub fn new(inner: T, name: SmolStr) -> Self { + Self { inner, name } + } +} + +#[serde_as] +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +#[serde(tag = "kind")] +pub struct DojoContract { + #[serde_as(as = "Option")] + pub address: Option, + #[serde_as(as = "UfeHex")] + pub class_hash: FieldElement, + #[serde_as(as = "UfeHex")] + pub original_class_hash: FieldElement, + // base class hash used to deploy the contract + #[serde_as(as = "UfeHex")] + pub base_class_hash: FieldElement, + pub abi: Option, + pub reads: Vec, + pub writes: Vec, + pub computed: Vec, +} + +/// Represents a declaration of a model. +#[serde_as] +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +#[serde(tag = "kind")] +pub struct DojoModel { + pub members: Vec, + #[serde_as(as = "UfeHex")] + pub class_hash: FieldElement, + #[serde_as(as = "UfeHex")] + pub original_class_hash: FieldElement, + pub abi: Option, +} + +#[serde_as] +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +#[serde(tag = "kind")] +pub struct Contract { + #[serde_as(as = "UfeHex")] + pub class_hash: FieldElement, + #[serde_as(as = "UfeHex")] + pub original_class_hash: FieldElement, + pub abi: Option, + #[serde_as(as = "Option")] + pub address: Option, + #[serde_as(as = "Option")] + pub transaction_hash: Option, + pub block_number: Option, + // used by World contract + pub seed: Option, +} + +#[serde_as] +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +#[serde(tag = "kind")] +pub struct Class { + #[serde_as(as = "UfeHex")] + pub class_hash: FieldElement, + #[serde_as(as = "UfeHex")] + pub original_class_hash: FieldElement, + pub abi: Option, +} + +#[serde_as] +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct OverlayDojoContract { + pub name: SmolStr, + pub original_class_hash: Option, + pub reads: Option>, + pub writes: Option>, +} + +#[serde_as] +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct OverlayDojoModel { + pub name: SmolStr, + pub original_class_hash: Option, +} + +#[serde_as] +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct OverlayContract { + pub name: SmolStr, + pub original_class_hash: Option, +} + +#[serde_as] +#[derive(Clone, Default, Debug, Serialize, Deserialize)] +#[cfg_attr(test, derive(PartialEq))] +pub struct OverlayClass { + pub name: SmolStr, + pub original_class_hash: Option, +} + +// Types used by manifest + +/// Represents a model member. +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct Member { + /// Name of the member. + pub name: String, + /// Type of the member. + #[serde(rename = "type")] + pub ty: String, + pub key: bool, +} + +#[serde_as] +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct ComputedValueEntrypoint { + // Name of the contract containing the entrypoint + pub contract: SmolStr, + // Name of entrypoint to get computed value + pub entrypoint: SmolStr, + // Component to compute for + pub model: Option, +} + +impl From for Member { + fn from(m: dojo_types::schema::Member) -> Self { + Self { name: m.name, ty: m.ty.name(), key: m.key } + } +} + +/// System input ABI. +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct Input { + pub name: String, + #[serde(rename = "type")] + pub ty: String, +} + +/// System Output ABI. +#[derive(Clone, Default, Debug, Serialize, Deserialize, PartialEq)] +pub struct Output { + #[serde(rename = "type")] + pub ty: String, +} + +/// Format of the ABI into the manifest. +#[serde_as] +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(untagged)] +pub enum AbiFormat { + /// Only a relative path to the ABI file is stored. + Path(Utf8PathBuf), + /// The full ABI is embedded. + Embed(Vec), +} + +impl AbiFormat { + /// Get the [`Utf8PathBuf`] if the ABI is stored as a path. + pub fn to_path(&self) -> Option<&Utf8PathBuf> { + match self { + AbiFormat::Path(p) => Some(p), + AbiFormat::Embed(_) => None, + } + } + + /// Loads an ABI from the path or embedded entries. + /// + /// # Arguments + /// + /// * `root_dir` - The root directory of the ABI file. + pub fn load_abi_string(&self, root_dir: &Utf8PathBuf) -> Result { + match self { + AbiFormat::Path(abi_path) => Ok(fs::read_to_string(root_dir.join(abi_path))?), + AbiFormat::Embed(abi) => Ok(serde_json::to_string(&abi)?), + } + } + + /// Convert to embed variant. + /// + /// # Arguments + /// + /// * `root_dir` - The root directory for the abi file resolution. + pub fn to_embed(&self, root_dir: &Utf8PathBuf) -> Result { + if let AbiFormat::Path(abi_path) = self { + let mut abi_file = std::fs::File::open(root_dir.join(abi_path))?; + Ok(serde_json::from_reader(&mut abi_file)?) + } else { + Ok(self.clone()) + } + } +} + +#[cfg(test)] +impl PartialEq for AbiFormat { + fn eq(&self, other: &Self) -> bool { + match (self, other) { + (AbiFormat::Path(p1), AbiFormat::Path(p2)) => p1 == p2, + (AbiFormat::Embed(e1), AbiFormat::Embed(e2)) => { + // Currently, [`AbiEntry`] does not implement [`PartialEq`] so we cannot compare + // them directly. + let e1_json = serde_json::to_string(e1).expect("valid JSON from ABI"); + let e2_json = serde_json::to_string(e2).expect("valid JSON from ABI"); + e1_json == e2_json + } + _ => false, + } + } +} diff --git a/crates/dojo-world/src/migration/class.rs b/crates/dojo-world/src/migration/class.rs index dfcb98acb5..d3f36fb096 100644 --- a/crates/dojo-world/src/migration/class.rs +++ b/crates/dojo-world/src/migration/class.rs @@ -11,6 +11,7 @@ use super::{Declarable, MigrationType, StateDiff}; pub struct ClassDiff { pub name: String, pub local: FieldElement, + pub original: FieldElement, pub remote: Option, } diff --git a/crates/dojo-world/src/migration/contract.rs b/crates/dojo-world/src/migration/contract.rs index ee8fc2dae7..166fbfbed8 100644 --- a/crates/dojo-world/src/migration/contract.rs +++ b/crates/dojo-world/src/migration/contract.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use async_trait::async_trait; use starknet::core::types::{DeclareTransactionResult, FieldElement}; -use super::{Declarable, Deployable, MigrationType, StateDiff}; +use super::{Declarable, Deployable, MigrationType, StateDiff, Upgradable}; pub type DeclareOutput = DeclareTransactionResult; @@ -12,23 +12,31 @@ pub type DeclareOutput = DeclareTransactionResult; #[derive(Debug, Default, Clone)] pub struct ContractDiff { pub name: String, - pub local: FieldElement, - pub remote: Option, + pub local_class_hash: FieldElement, + pub original_class_hash: FieldElement, + pub base_class_hash: FieldElement, + pub remote_class_hash: Option, } impl StateDiff for ContractDiff { fn is_same(&self) -> bool { - if let Some(remote) = self.remote { self.local == remote } else { false } + if let Some(remote) = self.remote_class_hash { + self.local_class_hash == remote + } else { + false + } } } impl Display for ContractDiff { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "{}:", self.name)?; - writeln!(f, " Local: {:#x}", self.local)?; + writeln!(f, " Local Class Hash: {:#x}", self.local_class_hash)?; + writeln!(f, " Original Class Hash: {:#x}", self.original_class_hash)?; + writeln!(f, " Base Class Hash: {:#x}", self.base_class_hash)?; - if let Some(remote) = self.remote { - writeln!(f, " Remote: {remote:#x}")?; + if let Some(remote) = self.remote_class_hash { + writeln!(f, " Remote Class Hash: {remote:#x}")?; } Ok(()) @@ -46,11 +54,11 @@ pub struct ContractMigration { impl ContractMigration { pub fn migration_type(&self) -> MigrationType { - let Some(remote) = self.diff.remote else { + let Some(remote) = self.diff.remote_class_hash else { return MigrationType::New; }; - match self.diff.local == remote { + match self.diff.local_class_hash == remote { true => MigrationType::New, false => MigrationType::Update, } @@ -70,3 +78,6 @@ impl Deployable for ContractMigration { self.salt } } + +#[async_trait] +impl Upgradable for ContractMigration {} diff --git a/crates/dojo-world/src/migration/mod.rs b/crates/dojo-world/src/migration/mod.rs index bafb4d25dc..471e16ac50 100644 --- a/crates/dojo-world/src/migration/mod.rs +++ b/crates/dojo-world/src/migration/mod.rs @@ -9,12 +9,10 @@ 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, FunctionCall, + BlockId, BlockTag, DeclareTransactionResult, FieldElement, FlattenedSierraClass, InvokeTransactionResult, MaybePendingTransactionReceipt, StarknetError, TransactionReceipt, }; -use starknet::core::utils::{ - get_contract_address, get_selector_from_name, CairoShortStringToFeltError, -}; +use starknet::core::utils::{get_contract_address, CairoShortStringToFeltError}; use starknet::macros::{felt, selector}; use starknet::providers::{Provider, ProviderError}; use starknet::signers::Signer; @@ -35,6 +33,17 @@ pub struct DeployOutput { pub block_number: Option, pub contract_address: FieldElement, pub declare: Option, + // base class hash at time of deployment + pub base_class_hash: FieldElement, + pub was_upgraded: bool, +} + +#[derive(Clone, Debug)] +pub struct UpgradeOutput { + pub transaction_hash: FieldElement, + pub block_number: Option, + pub contract_address: FieldElement, + pub declare: Option, } #[derive(Debug)] @@ -136,10 +145,11 @@ 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( + async fn deploy_dojo_contract( &self, world_address: FieldElement, class_hash: FieldElement, + base_class_hash: FieldElement, account: &SingleOwnerAccount, txn_config: TxConfig, ) -> Result as Account>::SignError>> @@ -153,32 +163,25 @@ pub trait Deployable: Declarable + Sync { 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::Pending), - ) - .await - .map_err(MigrationError::Provider)?; - let contract_address = - get_contract_address(self.salt(), base_class_hash[0], &[], world_address); + get_contract_address(self.salt(), base_class_hash, &[], world_address); + + let mut was_upgraded = false; let call = match account .provider() .get_class_hash_at(BlockId::Tag(BlockTag::Pending), contract_address) .await { - Ok(current_class_hash) if current_class_hash != class_hash => Call { - calldata: vec![contract_address, class_hash], - selector: selector!("upgrade_contract"), - to: world_address, - }, + Ok(current_class_hash) if current_class_hash != class_hash => { + was_upgraded = true; + + Call { + calldata: vec![contract_address, class_hash], + selector: selector!("upgrade_contract"), + to: world_address, + } + } Err(ProviderError::StarknetError(StarknetError::ContractNotFound)) => Call { calldata: vec![self.salt(), class_hash], @@ -205,7 +208,14 @@ pub trait Deployable: Declarable + Sync { let receipt = TransactionWaiter::new(transaction_hash, account.provider()).await?; let block_number = get_block_number_from_receipt(receipt); - Ok(DeployOutput { transaction_hash, block_number, contract_address, declare }) + Ok(DeployOutput { + transaction_hash, + block_number, + contract_address, + declare, + base_class_hash, + was_upgraded, + }) } async fn deploy( @@ -270,12 +280,78 @@ pub trait Deployable: Declarable + Sync { let receipt = TransactionWaiter::new(transaction_hash, account.provider()).await?; let block_number = get_block_number_from_receipt(receipt); - Ok(DeployOutput { transaction_hash, block_number, contract_address, declare }) + Ok(DeployOutput { + transaction_hash, + block_number, + contract_address, + declare, + base_class_hash: FieldElement::default(), + was_upgraded: false, + }) } fn salt(&self) -> FieldElement; } +#[cfg_attr(not(target_arch = "wasm32"), async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait(?Send))] +pub trait Upgradable: Deployable + Declarable + Sync { + async fn upgrade_world( + &self, + class_hash: FieldElement, + original_class_hash: FieldElement, + original_base_class_hash: FieldElement, + account: &SingleOwnerAccount, + txn_config: TxConfig, + ) -> Result as Account>::SignError>> + 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 original_constructor_calldata = vec![original_base_class_hash]; + let contract_address = get_contract_address( + self.salt(), + original_class_hash, + &original_constructor_calldata, + FieldElement::ZERO, + ); + + match account + .provider() + .get_class_hash_at(BlockId::Tag(BlockTag::Pending), contract_address) + .await + { + Ok(_) => {} + Err(e) => return Err(MigrationError::Provider(e)), + } + + let calldata = vec![class_hash]; + let mut txn = account.execute(vec![Call { + calldata, + selector: selector!("upgrade"), + to: contract_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)?; + + let receipt = TransactionWaiter::new(transaction_hash, account.provider()).await?; + let block_number = get_block_number_from_receipt(receipt); + + Ok(UpgradeOutput { transaction_hash, block_number, contract_address, declare }) + } +} + fn prepare_contract_declaration_params( artifact_path: &PathBuf, ) -> Result<(FlattenedSierraClass, FieldElement)> { diff --git a/crates/dojo-world/src/migration/strategy.rs b/crates/dojo-world/src/migration/strategy.rs index 0873e962de..6925a54ede 100644 --- a/crates/dojo-world/src/migration/strategy.rs +++ b/crates/dojo-world/src/migration/strategy.rs @@ -104,8 +104,8 @@ pub fn prepare_for_migration( world.salt = salt; world.contract_address = get_contract_address( salt, - diff.world.local, - &[base.as_ref().unwrap().diff.local], + diff.world.original_class_hash, + &[base.as_ref().unwrap().diff.original], FieldElement::ZERO, ); } @@ -153,8 +153,10 @@ fn evaluate_contracts_to_migrate( let mut comps_to_migrate = vec![]; for c in contracts { - match c.remote { - Some(remote) if remote == c.local && !world_contract_will_migrate => continue, + match c.remote_class_hash { + Some(remote) if remote == c.local_class_hash && !world_contract_will_migrate => { + continue; + } _ => { let path = find_artifact_path(c.name.as_str(), artifact_paths)?; comps_to_migrate.push(ContractMigration { @@ -176,8 +178,8 @@ fn evaluate_contract_to_migrate( world_contract_will_migrate: bool, ) -> Result> { if world_contract_will_migrate - || contract.remote.is_none() - || matches!(contract.remote, Some(remote_hash) if remote_hash != contract.local) + || contract.remote_class_hash.is_none() + || matches!(contract.remote_class_hash, Some(remote_hash) if remote_hash != contract.local_class_hash) { let path = find_artifact_path(&contract.name, artifact_paths)?; diff --git a/crates/dojo-world/src/migration/world.rs b/crates/dojo-world/src/migration/world.rs index 417258fb50..b8a508f6e3 100644 --- a/crates/dojo-world/src/migration/world.rs +++ b/crates/dojo-world/src/migration/world.rs @@ -1,6 +1,7 @@ use std::fmt::Display; use convert_case::{Case, Casing}; +use starknet_crypto::FieldElement; use super::class::ClassDiff; use super::contract::ContractDiff; @@ -30,6 +31,7 @@ impl WorldDiff { .map(|model| ClassDiff { name: model.name.to_string(), local: *model.inner.class_hash(), + original: *model.inner.original_class_hash(), remote: remote.as_ref().and_then(|m| { // Remote models are detected from events, where only the struct // name (pascal case) is emitted. @@ -51,28 +53,44 @@ impl WorldDiff { let contracts = local .contracts .iter() - .map(|contract| ContractDiff { - name: contract.name.to_string(), - local: *contract.inner.class_hash(), - remote: remote.as_ref().and_then(|m| { - m.contracts - .iter() - .find(|r| r.inner.class_hash() == contract.inner.class_hash()) - .map(|r| *r.inner.class_hash()) - }), + .map(|contract| { + let base_class_hash = { + let class_hash = contract.inner.base_class_hash; + if class_hash != FieldElement::ZERO { + class_hash + } else { + *local.base.inner.class_hash() + } + }; + + ContractDiff { + name: contract.name.to_string(), + local_class_hash: *contract.inner.class_hash(), + original_class_hash: *contract.inner.original_class_hash(), + base_class_hash, + remote_class_hash: remote.as_ref().and_then(|m| { + m.contracts + .iter() + .find(|r| r.inner.class_hash() == contract.inner.class_hash()) + .map(|r| *r.inner.class_hash()) + }), + } }) .collect::>(); let base = ClassDiff { name: BASE_CONTRACT_NAME.into(), local: *local.base.inner.class_hash(), + original: *local.base.inner.original_class_hash(), remote: remote.as_ref().map(|m| *m.base.inner.class_hash()), }; let world = ContractDiff { name: WORLD_CONTRACT_NAME.into(), - local: *local.world.inner.class_hash(), - remote: remote.map(|m| *m.world.inner.class_hash()), + local_class_hash: *local.world.inner.class_hash(), + original_class_hash: *local.world.inner.original_class_hash(), + base_class_hash: *local.base.inner.class_hash(), + remote_class_hash: remote.map(|m| *m.world.inner.class_hash()), }; WorldDiff { world, base, contracts, models } diff --git a/crates/sozo/ops/src/migration/mod.rs b/crates/sozo/ops/src/migration/mod.rs index af9c6d5917..1b593e23c0 100644 --- a/crates/sozo/ops/src/migration/mod.rs +++ b/crates/sozo/ops/src/migration/mod.rs @@ -16,6 +16,7 @@ use dojo_world::migration::strategy::{generate_salt, prepare_for_migration, Migr use dojo_world::migration::world::WorldDiff; use dojo_world::migration::{ Declarable, DeployOutput, Deployable, MigrationError, RegisterOutput, StateDiff, TxConfig, + Upgradable, UpgradeOutput, }; use dojo_world::utils::TransactionWaiter; use scarb::core::Workspace; @@ -48,6 +49,8 @@ pub struct MigrationOutput { // Represents if full migration got completeled. // If false that means migration got partially completed. pub full: bool, + + pub contracts: Vec>, } pub async fn migrate( @@ -106,7 +109,6 @@ where update_manifests_and_abis( ws, local_manifest, - remote_manifest, &manifest_dir, migration_output, &chain_id, @@ -118,7 +120,6 @@ where update_manifests_and_abis( ws, local_manifest, - remote_manifest, &manifest_dir, MigrationOutput { world_address, ..Default::default() }, &chain_id, @@ -140,7 +141,6 @@ fn build_deployed_path(manifest_dir: &Utf8PathBuf, chain_id: &str, extension: &s async fn update_manifests_and_abis( ws: &Workspace<'_>, local_manifest: BaseManifest, - remote_manifest: Option, manifest_dir: &Utf8PathBuf, migration_output: MigrationOutput, chain_id: &str, @@ -171,16 +171,25 @@ async fn update_manifests_and_abis( local_manifest.world.inner.block_number = migration_output.world_block_number; } - let base_class_hash = match remote_manifest { - Some(manifest) => *manifest.base.inner.class_hash(), - None => *local_manifest.base.inner.class_hash(), - }; + let base_class_hash = *local_manifest.base.inner.class_hash(); + + debug_assert!(local_manifest.contracts.len() == migration_output.contracts.len()); + + local_manifest.contracts.iter_mut().zip(migration_output.contracts).for_each( + |(local_manifest, contract_output)| { + let salt = generate_salt(&local_manifest.name); + local_manifest.inner.address = Some(get_contract_address( + salt, + base_class_hash, + &[], + migration_output.world_address, + )); - local_manifest.contracts.iter_mut().for_each(|c| { - let salt = generate_salt(&c.name); - c.inner.address = - Some(get_contract_address(salt, base_class_hash, &[], migration_output.world_address)); - }); + if let Some(output) = contract_output { + local_manifest.inner.base_class_hash = output.base_class_hash; + } + }, + ); // copy abi files from `abi/base` to `abi/deployments/{chain_id}` and update abi path in // local_manifest @@ -304,7 +313,7 @@ where if overlay_path.exists() { let overlay_manifest = OverlayManifest::load_from_path(&manifest_dir.join(MANIFESTS_DIR).join(OVERLAYS_DIR)) - .map_err(|_| anyhow!("Fail to load overlay manifest file."))?; + .map_err(|e| anyhow!("Fail to load overlay manifest file: {e}."))?; // merge user defined changes to base manifest local_manifest.merge(overlay_manifest); @@ -411,68 +420,53 @@ where Some(world) => { ui.print_header("# World"); - let calldata = vec![strategy.base.as_ref().unwrap().diff.local]; - let deploy_result = - deploy_contract(world, "world", calldata.clone(), migrator, &ui, &txn_config) - .await - .map_err(|e| { - ui.verbose(format!("{e:?}")); - anyhow!("Failed to deploy world: {e}") - })?; - - (world_tx_hash, world_block_number) = - if let ContractDeploymentOutput::Output(deploy_result) = deploy_result { - (Some(deploy_result.transaction_hash), deploy_result.block_number) - } else { - (None, None) - }; - - ui.print_sub(format!("Contract address: {:#x}", world.contract_address)); - - let offline = ws.config().offline(); + // If a migration is pending for the world, we upgrade only if the remote world + // already exists. + if world.diff.remote_class_hash.is_some() { + let _deploy_result = upgrade_contract( + world, + "world", + world.diff.original_class_hash, + strategy.base.as_ref().unwrap().diff.original, + migrator, + &ui, + &txn_config, + ) + .await + .map_err(|e| { + ui.verbose(format!("{e:?}")); + anyhow!("Failed to upgrade world: {e}") + })?; - if offline { - ui.print_sub("Skipping metadata upload because of offline mode"); + ui.print_sub(format!( + "Upgraded Contract at address: {:#x}", + world.contract_address + )); } else { - let metadata = dojo_metadata_from_workspace(ws); - if let Some(meta) = metadata.as_ref().and_then(|inner| inner.world()) { - match meta.upload().await { - Ok(hash) => { - let mut encoded_uri = - cairo_utils::encode_uri(&format!("ipfs://{hash}"))?; - - // Metadata is expecting an array of capacity 3. - if encoded_uri.len() < 3 { - encoded_uri.extend(vec![FieldElement::ZERO; 3 - encoded_uri.len()]); - } - - let world_metadata = ResourceMetadata { - resource_id: FieldElement::ZERO, - metadata_uri: encoded_uri, - }; - - let InvokeTransactionResult { transaction_hash } = - WorldContract::new(world.contract_address, migrator) - .set_metadata(&world_metadata) - .send() - .await - .map_err(|e| { - ui.verbose(format!("{e:?}")); - anyhow!("Failed to set World metadata: {e}") - })?; - - TransactionWaiter::new(transaction_hash, migrator.provider()).await?; - - ui.print_sub(format!( - "Set Metadata transaction: {:#x}", - transaction_hash - )); - ui.print_sub(format!("Metadata uri: ipfs://{hash}")); - } - Err(err) => { - ui.print_sub(format!("Failed to set World metadata:\n{err}")); - } - } + let calldata = vec![strategy.base.as_ref().unwrap().diff.local]; + let deploy_result = + deploy_contract(world, "world", calldata.clone(), migrator, &ui, &txn_config) + .await + .map_err(|e| { + ui.verbose(format!("{e:?}")); + anyhow!("Failed to deploy world: {e}") + })?; + + (world_tx_hash, world_block_number) = + if let ContractDeploymentOutput::Output(deploy_result) = deploy_result { + (Some(deploy_result.transaction_hash), deploy_result.block_number) + } else { + (None, None) + }; + + ui.print_sub(format!("Contract address: {:#x}", world.contract_address)); + + let offline = ws.config().offline(); + + if offline { + ui.print_sub("Skipping metadata upload because of offline mode"); + } else { + upload_metadata(ws, world, migrator, &ui).await?; } } } @@ -484,6 +478,7 @@ where world_tx_hash, world_block_number, full: false, + contracts: vec![], }; // Once Torii supports indexing arrays, we should declare and register the @@ -496,8 +491,11 @@ where return Ok(migration_output); } } - match deploy_contracts(strategy, migrator, &ui, txn_config).await { - Ok(_) => (), + + match deploy_dojo_contracts(strategy, migrator, &ui, txn_config).await { + Ok(res) => { + migration_output.contracts = res; + } Err(e) => { ui.anyhow(&e); return Ok(migration_output); @@ -509,11 +507,62 @@ where Ok(migration_output) } +async fn upload_metadata( + ws: &Workspace<'_>, + world: &ContractMigration, + migrator: &SingleOwnerAccount, + ui: &Ui, +) -> Result<(), anyhow::Error> +where + P: Provider + Sync + Send + 'static, + S: Signer + Sync + Send + 'static, +{ + let metadata = dojo_metadata_from_workspace(ws); + if let Some(meta) = metadata.as_ref().and_then(|inner| inner.world()) { + match meta.upload().await { + Ok(hash) => { + let mut encoded_uri = cairo_utils::encode_uri(&format!("ipfs://{hash}"))?; + + // Metadata is expecting an array of capacity 3. + if encoded_uri.len() < 3 { + encoded_uri.extend(vec![FieldElement::ZERO; 3 - encoded_uri.len()]); + } + + let world_metadata = + ResourceMetadata { resource_id: FieldElement::ZERO, metadata_uri: encoded_uri }; + + let InvokeTransactionResult { transaction_hash } = + WorldContract::new(world.contract_address, migrator) + .set_metadata(&world_metadata) + .send() + .await + .map_err(|e| { + ui.verbose(format!("{e:?}")); + anyhow!("Failed to set World metadata: {e}") + })?; + + TransactionWaiter::new(transaction_hash, migrator.provider()).await?; + + ui.print_sub(format!("Set Metadata transaction: {:#x}", transaction_hash)); + ui.print_sub(format!("Metadata uri: ipfs://{hash}")); + } + Err(err) => { + ui.print_sub(format!("Failed to set World metadata:\n{err}")); + } + } + } + Ok(()) +} + enum ContractDeploymentOutput { AlreadyDeployed(FieldElement), Output(DeployOutput), } +enum ContractUpgradeOutput { + Output(UpgradeOutput), +} + async fn deploy_contract( contract: &ContractMigration, contract_id: &str, @@ -527,7 +576,12 @@ where S: Signer + Sync + Send + 'static, { match contract - .deploy(contract.diff.local, constructor_calldata, migrator, txn_config.unwrap_or_default()) + .deploy( + contract.diff.local_class_hash, + constructor_calldata, + migrator, + txn_config.unwrap_or_default(), + ) .await { Ok(val) => { @@ -555,6 +609,51 @@ where } } +async fn upgrade_contract( + contract: &ContractMigration, + contract_id: &str, + original_class_hash: FieldElement, + original_base_class_hash: FieldElement, + migrator: &SingleOwnerAccount, + ui: &Ui, + txn_config: &Option, +) -> Result +where + P: Provider + Sync + Send + 'static, + S: Signer + Sync + Send + 'static, +{ + match contract + .upgrade_world( + contract.diff.local_class_hash, + original_class_hash, + original_base_class_hash, + migrator, + (*txn_config).unwrap_or_default(), + ) + .await + { + Ok(val) => { + if let Some(declare) = val.clone().declare { + ui.print_hidden_sub(format!( + "Declare transaction: {:#x}", + declare.transaction_hash + )); + } + + ui.print_hidden_sub(format!("Upgrade transaction: {:#x}", val.transaction_hash)); + + Ok(ContractUpgradeOutput::Output(val)) + } + Err(MigrationError::ArtifactError(e)) => { + return Err(handle_artifact_error(ui, contract.artifact_path(), e)); + } + Err(e) => { + ui.verbose(format!("{e:?}")); + Err(anyhow!("Failed to upgrade {contract_id}: {e}")) + } + } +} + async fn register_models( strategy: &MigrationStrategy, migrator: &SingleOwnerAccount, @@ -624,7 +723,7 @@ where Ok(Some(RegisterOutput { transaction_hash, declare_output })) } -async fn deploy_contracts( +async fn deploy_dojo_contracts( strategy: &mut MigrationStrategy, migrator: &SingleOwnerAccount, ui: &Ui, @@ -651,9 +750,10 @@ where let name = &contract.diff.name; ui.print(italic_message(name).to_string()); match contract - .world_deploy( + .deploy_dojo_contract( world_address, - contract.diff.local, + contract.diff.local_class_hash, + contract.diff.base_class_hash, migrator, txn_config.unwrap_or_default(), ) @@ -668,8 +768,24 @@ where } contract.contract_address = output.contract_address; - ui.print_hidden_sub(format!("Deploy transaction: {:#x}", output.transaction_hash)); - ui.print_sub(format!("Contract address: {:#x}", output.contract_address)); + + if output.was_upgraded { + ui.print_hidden_sub(format!( + "Invoke transaction to upgrade: {:#x}", + output.transaction_hash + )); + ui.print_sub(format!( + "Contract address [upgraded]: {:#x}", + output.contract_address + )); + } else { + ui.print_hidden_sub(format!( + "Deploy transaction: {:#x}", + output.transaction_hash + )); + ui.print_sub(format!("Contract address: {:#x}", output.contract_address)); + } + deploy_output.push(Some(output)); } Err(MigrationError::ContractAlreadyDeployed(contract_address)) => { @@ -727,7 +843,7 @@ where .get_class_hash_at(BlockId::Tag(BlockTag::Pending), contract_address) .await { - Ok(current_class_hash) if current_class_hash != contract.diff.local => { + Ok(current_class_hash) if current_class_hash != contract.diff.local_class_hash => { return format!("upgrade {}", contract.diff.name); } Err(ProviderError::StarknetError(StarknetError::ContractNotFound)) => { @@ -754,7 +870,7 @@ where if let Some(world) = &strategy.world { ui.print_header("# World"); - ui.print_sub(format!("declare (class hash: {:#x})\n", world.diff.local)); + ui.print_sub(format!("declare (class hash: {:#x})\n", world.diff.local_class_hash)); } if !&strategy.models.is_empty() { @@ -769,7 +885,7 @@ where ui.print_header(format!("# Contracts ({})", &strategy.contracts.len())); for c in &strategy.contracts { let op_name = get_contract_operation_name(provider, c, strategy.world_address).await; - ui.print_sub(format!("{op_name} (class hash: {:#x})", c.diff.local)); + ui.print_sub(format!("{op_name} (class hash: {:#x})", c.diff.local_class_hash)); } ui.print(" "); } diff --git a/crates/torii/client/src/client/mod.rs b/crates/torii/client/src/client/mod.rs index 3d6f369a21..a42f4ce663 100644 --- a/crates/torii/client/src/client/mod.rs +++ b/crates/torii/client/src/client/mod.rs @@ -11,7 +11,6 @@ use dojo_types::schema::Ty; use dojo_types::WorldMetadata; use dojo_world::contracts::WorldContractReader; use futures::lock::Mutex; -use futures::Future; use parking_lot::{RwLock, RwLockReadGuard}; use starknet::core::utils::cairo_short_string_to_felt; use starknet::providers::jsonrpc::HttpTransport; diff --git a/crates/torii/types-test/manifests/base/base.toml b/crates/torii/types-test/manifests/base/base.toml index 05caa48e93..d926bca4d7 100644 --- a/crates/torii/types-test/manifests/base/base.toml +++ b/crates/torii/types-test/manifests/base/base.toml @@ -1,3 +1,4 @@ kind = "Class" class_hash = "0x679177a2cb757694ac4f326d01052ff0963eac0bc2a17116a2b87badcdf6f76" +original_class_hash = "0x679177a2cb757694ac4f326d01052ff0963eac0bc2a17116a2b87badcdf6f76" name = "dojo::base::base" diff --git a/crates/torii/types-test/manifests/base/contracts/records.toml b/crates/torii/types-test/manifests/base/contracts/records.toml index e8c9f449f1..81acd61696 100644 --- a/crates/torii/types-test/manifests/base/contracts/records.toml +++ b/crates/torii/types-test/manifests/base/contracts/records.toml @@ -1,5 +1,7 @@ kind = "DojoContract" class_hash = "0x658309df749cea1c32e21920740011e829626ab06c9b4d0c05b75f82a20693b" +original_class_hash = "0x658309df749cea1c32e21920740011e829626ab06c9b4d0c05b75f82a20693b" +base_class_hash = "0x0" abi = "abis/base/contracts/records.json" reads = [] writes = [] diff --git a/crates/torii/types-test/manifests/base/models/record.toml b/crates/torii/types-test/manifests/base/models/record.toml index d87b0a1012..7cf850f9fc 100644 --- a/crates/torii/types-test/manifests/base/models/record.toml +++ b/crates/torii/types-test/manifests/base/models/record.toml @@ -1,5 +1,6 @@ kind = "DojoModel" class_hash = "0x134456282bbaf00e0895ff43f286af8d490202baf6279d2b05be9bc0c05f059" +original_class_hash = "0x134456282bbaf00e0895ff43f286af8d490202baf6279d2b05be9bc0c05f059" abi = "abis/base/models/record.json" name = "types_test::models::record" diff --git a/crates/torii/types-test/manifests/base/models/record_sibling.toml b/crates/torii/types-test/manifests/base/models/record_sibling.toml index c8c21009fd..f0333a30cb 100644 --- a/crates/torii/types-test/manifests/base/models/record_sibling.toml +++ b/crates/torii/types-test/manifests/base/models/record_sibling.toml @@ -1,5 +1,6 @@ kind = "DojoModel" class_hash = "0x4e92336e21ac7970b9bd9f4e294705f7864c0b29f53fdbf42ff7a9d7f0a53f3" +original_class_hash = "0x4e92336e21ac7970b9bd9f4e294705f7864c0b29f53fdbf42ff7a9d7f0a53f3" abi = "abis/base/models/record_sibling.json" name = "types_test::models::record_sibling" diff --git a/crates/torii/types-test/manifests/base/models/subrecord.toml b/crates/torii/types-test/manifests/base/models/subrecord.toml index ac7d6e5d50..4aa42d670d 100644 --- a/crates/torii/types-test/manifests/base/models/subrecord.toml +++ b/crates/torii/types-test/manifests/base/models/subrecord.toml @@ -1,5 +1,6 @@ kind = "DojoModel" class_hash = "0x7a47c3a9c8509a1d4a0379e50799eba7b173db6e41961341fe3f856a51d627" +original_class_hash = "0x7a47c3a9c8509a1d4a0379e50799eba7b173db6e41961341fe3f856a51d627" abi = "abis/base/models/subrecord.json" name = "types_test::models::subrecord" diff --git a/crates/torii/types-test/manifests/base/world.toml b/crates/torii/types-test/manifests/base/world.toml index 62f17814b4..e3cdf0e901 100644 --- a/crates/torii/types-test/manifests/base/world.toml +++ b/crates/torii/types-test/manifests/base/world.toml @@ -1,4 +1,5 @@ kind = "Class" class_hash = "0x799bc4e9da10bfb3dd88e6f223c9cfbf7745435cd14f5d69675ea448e578cd" +original_class_hash = "0x799bc4e9da10bfb3dd88e6f223c9cfbf7745435cd14f5d69675ea448e578cd" abi = "abis/base/world.json" name = "dojo::world::world" diff --git a/examples/spawn-and-move/manifests/base/base.toml b/examples/spawn-and-move/manifests/base/base.toml index 05caa48e93..d926bca4d7 100644 --- a/examples/spawn-and-move/manifests/base/base.toml +++ b/examples/spawn-and-move/manifests/base/base.toml @@ -1,3 +1,4 @@ kind = "Class" class_hash = "0x679177a2cb757694ac4f326d01052ff0963eac0bc2a17116a2b87badcdf6f76" +original_class_hash = "0x679177a2cb757694ac4f326d01052ff0963eac0bc2a17116a2b87badcdf6f76" name = "dojo::base::base" diff --git a/examples/spawn-and-move/manifests/base/contracts/actions.toml b/examples/spawn-and-move/manifests/base/contracts/actions.toml index 746ce31be6..f10132f153 100644 --- a/examples/spawn-and-move/manifests/base/contracts/actions.toml +++ b/examples/spawn-and-move/manifests/base/contracts/actions.toml @@ -1,5 +1,7 @@ kind = "DojoContract" class_hash = "0x16b1037beb348c3cf11c7f10733d366c7f29bc9d9172ba663421e2af4dab83c" +original_class_hash = "0x16b1037beb348c3cf11c7f10733d366c7f29bc9d9172ba663421e2af4dab83c" +base_class_hash = "0x0" abi = "abis/base/contracts/actions.json" reads = [] writes = [] diff --git a/examples/spawn-and-move/manifests/base/models/moved.toml b/examples/spawn-and-move/manifests/base/models/moved.toml index ad3dee2b6b..b92a6b8fee 100644 --- a/examples/spawn-and-move/manifests/base/models/moved.toml +++ b/examples/spawn-and-move/manifests/base/models/moved.toml @@ -1,5 +1,6 @@ kind = "DojoModel" class_hash = "0x52659850f9939482810d9f6b468b91dc99e0b7fa42c2016cf12833ec06ce911" +original_class_hash = "0x52659850f9939482810d9f6b468b91dc99e0b7fa42c2016cf12833ec06ce911" abi = "abis/base/models/moved.json" name = "dojo_examples::actions::actions::moved" diff --git a/examples/spawn-and-move/manifests/base/models/moves.toml b/examples/spawn-and-move/manifests/base/models/moves.toml index 19f7591bef..108b422e17 100644 --- a/examples/spawn-and-move/manifests/base/models/moves.toml +++ b/examples/spawn-and-move/manifests/base/models/moves.toml @@ -1,5 +1,6 @@ kind = "DojoModel" class_hash = "0x511fbd833938f5c4b743eea1e67605a125d7ff60e8a09e8dc227ad2fb59ca54" +original_class_hash = "0x511fbd833938f5c4b743eea1e67605a125d7ff60e8a09e8dc227ad2fb59ca54" abi = "abis/base/models/moves.json" name = "dojo_examples::models::moves" diff --git a/examples/spawn-and-move/manifests/base/models/position.toml b/examples/spawn-and-move/manifests/base/models/position.toml index 270f3e4ca8..05dd5108d3 100644 --- a/examples/spawn-and-move/manifests/base/models/position.toml +++ b/examples/spawn-and-move/manifests/base/models/position.toml @@ -1,5 +1,6 @@ kind = "DojoModel" class_hash = "0xb33ae053213ccb2a57967ffc4411901f3efab24781ca867adcd0b90f2fece5" +original_class_hash = "0xb33ae053213ccb2a57967ffc4411901f3efab24781ca867adcd0b90f2fece5" abi = "abis/base/models/position.json" name = "dojo_examples::models::position" diff --git a/examples/spawn-and-move/manifests/base/world.toml b/examples/spawn-and-move/manifests/base/world.toml index 62f17814b4..e3cdf0e901 100644 --- a/examples/spawn-and-move/manifests/base/world.toml +++ b/examples/spawn-and-move/manifests/base/world.toml @@ -1,4 +1,5 @@ kind = "Class" class_hash = "0x799bc4e9da10bfb3dd88e6f223c9cfbf7745435cd14f5d69675ea448e578cd" +original_class_hash = "0x799bc4e9da10bfb3dd88e6f223c9cfbf7745435cd14f5d69675ea448e578cd" abi = "abis/base/world.json" name = "dojo::world::world" diff --git a/examples/spawn-and-move/manifests/deployments/KATANA.json b/examples/spawn-and-move/manifests/deployments/KATANA.json index a2a1968e95..ce3e003c8d 100644 --- a/examples/spawn-and-move/manifests/deployments/KATANA.json +++ b/examples/spawn-and-move/manifests/deployments/KATANA.json @@ -2,6 +2,7 @@ "world": { "kind": "Contract", "class_hash": "0x799bc4e9da10bfb3dd88e6f223c9cfbf7745435cd14f5d69675ea448e578cd", + "original_class_hash": "0x799bc4e9da10bfb3dd88e6f223c9cfbf7745435cd14f5d69675ea448e578cd", "abi": [ { "type": "impl", @@ -668,6 +669,7 @@ "base": { "kind": "Class", "class_hash": "0x679177a2cb757694ac4f326d01052ff0963eac0bc2a17116a2b87badcdf6f76", + "original_class_hash": "0x679177a2cb757694ac4f326d01052ff0963eac0bc2a17116a2b87badcdf6f76", "abi": null, "name": "dojo::base::base" }, @@ -676,6 +678,8 @@ "kind": "DojoContract", "address": "0x3539c9b89b08095ba914653fb0f20e55d4b172a415beade611bc260b346d0f7", "class_hash": "0x16b1037beb348c3cf11c7f10733d366c7f29bc9d9172ba663421e2af4dab83c", + "original_class_hash": "0x16b1037beb348c3cf11c7f10733d366c7f29bc9d9172ba663421e2af4dab83c", + "base_class_hash": "0x679177a2cb757694ac4f326d01052ff0963eac0bc2a17116a2b87badcdf6f76", "abi": [ { "type": "impl", @@ -918,10 +922,7 @@ ] } ], - "reads": [ - "Moves", - "Position" - ], + "reads": [], "writes": [], "computed": [], "name": "dojo_examples::actions::actions" @@ -943,6 +944,7 @@ } ], "class_hash": "0x52659850f9939482810d9f6b468b91dc99e0b7fa42c2016cf12833ec06ce911", + "original_class_hash": "0x52659850f9939482810d9f6b468b91dc99e0b7fa42c2016cf12833ec06ce911", "abi": [ { "type": "impl", @@ -1204,6 +1206,7 @@ } ], "class_hash": "0x511fbd833938f5c4b743eea1e67605a125d7ff60e8a09e8dc227ad2fb59ca54", + "original_class_hash": "0x511fbd833938f5c4b743eea1e67605a125d7ff60e8a09e8dc227ad2fb59ca54", "abi": [ { "type": "impl", @@ -1464,6 +1467,7 @@ } ], "class_hash": "0xb33ae053213ccb2a57967ffc4411901f3efab24781ca867adcd0b90f2fece5", + "original_class_hash": "0xb33ae053213ccb2a57967ffc4411901f3efab24781ca867adcd0b90f2fece5", "abi": [ { "type": "impl", diff --git a/examples/spawn-and-move/manifests/deployments/KATANA.toml b/examples/spawn-and-move/manifests/deployments/KATANA.toml index 44fe28bfa0..adab62ea87 100644 --- a/examples/spawn-and-move/manifests/deployments/KATANA.toml +++ b/examples/spawn-and-move/manifests/deployments/KATANA.toml @@ -1,6 +1,7 @@ [world] kind = "Contract" class_hash = "0x799bc4e9da10bfb3dd88e6f223c9cfbf7745435cd14f5d69675ea448e578cd" +original_class_hash = "0x799bc4e9da10bfb3dd88e6f223c9cfbf7745435cd14f5d69675ea448e578cd" abi = "abis/deployments/KATANA/world.json" address = "0x1385f25d20a724edc9c7b3bd9636c59af64cbaf9fcd12f33b3af96b2452f295" transaction_hash = "0x6afefdcc49b3563a4f3657900ba71e9f9356861b15b942a73f2018f046a1048" @@ -11,17 +12,17 @@ name = "dojo::world::world" [base] kind = "Class" class_hash = "0x679177a2cb757694ac4f326d01052ff0963eac0bc2a17116a2b87badcdf6f76" +original_class_hash = "0x679177a2cb757694ac4f326d01052ff0963eac0bc2a17116a2b87badcdf6f76" name = "dojo::base::base" [[contracts]] kind = "DojoContract" address = "0x3539c9b89b08095ba914653fb0f20e55d4b172a415beade611bc260b346d0f7" class_hash = "0x16b1037beb348c3cf11c7f10733d366c7f29bc9d9172ba663421e2af4dab83c" +original_class_hash = "0x16b1037beb348c3cf11c7f10733d366c7f29bc9d9172ba663421e2af4dab83c" +base_class_hash = "0x679177a2cb757694ac4f326d01052ff0963eac0bc2a17116a2b87badcdf6f76" abi = "abis/deployments/KATANA/contracts/actions.json" -reads = [ - "Moves", - "Position", -] +reads = [] writes = [] computed = [] name = "dojo_examples::actions::actions" @@ -29,6 +30,7 @@ name = "dojo_examples::actions::actions" [[models]] kind = "DojoModel" class_hash = "0x52659850f9939482810d9f6b468b91dc99e0b7fa42c2016cf12833ec06ce911" +original_class_hash = "0x52659850f9939482810d9f6b468b91dc99e0b7fa42c2016cf12833ec06ce911" abi = "abis/base/models/moved.json" name = "dojo_examples::actions::actions::moved" @@ -45,6 +47,7 @@ key = false [[models]] kind = "DojoModel" class_hash = "0x511fbd833938f5c4b743eea1e67605a125d7ff60e8a09e8dc227ad2fb59ca54" +original_class_hash = "0x511fbd833938f5c4b743eea1e67605a125d7ff60e8a09e8dc227ad2fb59ca54" abi = "abis/base/models/moves.json" name = "dojo_examples::models::moves" @@ -66,6 +69,7 @@ key = false [[models]] kind = "DojoModel" class_hash = "0xb33ae053213ccb2a57967ffc4411901f3efab24781ca867adcd0b90f2fece5" +original_class_hash = "0xb33ae053213ccb2a57967ffc4411901f3efab24781ca867adcd0b90f2fece5" abi = "abis/base/models/position.json" name = "dojo_examples::models::position" diff --git a/examples/spawn-and-move/manifests/overlays/contracts/actions.toml b/examples/spawn-and-move/manifests/overlays/contracts/actions.toml deleted file mode 100644 index 0eb4bb1a43..0000000000 --- a/examples/spawn-and-move/manifests/overlays/contracts/actions.toml +++ /dev/null @@ -1,2 +0,0 @@ -name = "dojo_examples::actions::actions" -reads = [ "Moves", "Position" ]