diff --git a/bin/sozo/src/commands/clean.rs b/bin/sozo/src/commands/clean.rs index a91a87aaf0..da6245c488 100644 --- a/bin/sozo/src/commands/clean.rs +++ b/bin/sozo/src/commands/clean.rs @@ -81,10 +81,9 @@ mod tests { migration::migrate( &ws, None, - "chain_id".to_string(), runner.endpoint(), &runner.account(0), - Some("dojo_examples".to_string()), + "dojo_examples", true, TxnConfig::default(), ) diff --git a/bin/sozo/src/commands/dev.rs b/bin/sozo/src/commands/dev.rs index 3aee618053..772ad87fcc 100644 --- a/bin/sozo/src/commands/dev.rs +++ b/bin/sozo/src/commands/dev.rs @@ -18,7 +18,7 @@ use notify_debouncer_mini::notify::RecursiveMode; use notify_debouncer_mini::{new_debouncer, DebouncedEvent, DebouncedEventKind}; use scarb::compiler::CompilationUnit; use scarb::core::{Config, Workspace}; -use sozo_ops::migration::{self, prepare_migration}; +use sozo_ops::migration; use starknet::accounts::SingleOwnerAccount; use starknet::core::types::FieldElement; use starknet::providers::Provider; @@ -52,9 +52,9 @@ pub struct DevArgs { impl DevArgs { pub fn run(self, config: &Config) -> Result<()> { - let env_metadata = if config.manifest_path().exists() { - let ws = scarb::ops::read_workspace(config.manifest_path(), config)?; + let ws = scarb::ops::read_workspace(config.manifest_path(), config)?; + let env_metadata = if config.manifest_path().exists() { dojo_metadata_from_workspace(&ws).env().cloned() } else { None @@ -68,11 +68,13 @@ impl DevArgs { config.manifest_path().parent().unwrap().as_std_path(), RecursiveMode::Recursive, )?; - let name = self.name.clone(); + + let name = self.name.unwrap_or_else(|| ws.root_package().unwrap().id.name.to_string()); + let mut previous_manifest: Option = Option::None; let result = build(&mut context); - let Some((mut world_address, account, _, _)) = context + let Some((mut world_address, account, _)) = context .ws .config() .tokio_handle() @@ -81,7 +83,7 @@ impl DevArgs { self.account, self.starknet, self.world, - name.as_ref(), + &name, env_metadata.as_ref(), )) .ok() @@ -92,7 +94,7 @@ impl DevArgs { match context.ws.config().tokio_handle().block_on(migrate( world_address, &account, - name.clone(), + &name, &context.ws, previous_manifest.clone(), )) { @@ -127,7 +129,7 @@ impl DevArgs { match context.ws.config().tokio_handle().block_on(migrate( world_address, &account, - name.clone(), + &name, &context.ws, previous_manifest.clone(), )) { @@ -215,7 +217,7 @@ fn build(context: &mut DevContext<'_>) -> Result<()> { async fn migrate( mut world_address: Option, account: &SingleOwnerAccount, - name: Option, + name: &str, ws: &Workspace<'_>, previous_manifest: Option, ) -> Result<(DeploymentManifest, Option)> @@ -245,7 +247,7 @@ where } let ui = ws.config().ui(); - let mut strategy = prepare_migration(&target_dir, diff, name, world_address, &ui)?; + let mut strategy = migration::prepare_migration(&target_dir, diff, name, world_address, &ui)?; match migration::apply_diff(ws, account, TxnConfig::default(), &mut strategy).await { Ok(migration_output) => { diff --git a/bin/sozo/src/commands/migrate.rs b/bin/sozo/src/commands/migrate.rs index b392ef1014..538de0fba8 100644 --- a/bin/sozo/src/commands/migrate.rs +++ b/bin/sozo/src/commands/migrate.rs @@ -22,44 +22,30 @@ use super::options::world::WorldOptions; pub struct MigrateArgs { #[command(subcommand)] pub command: MigrateCommand, + + #[arg(long, global = true)] + #[arg(help = "Name of the World.")] + #[arg(long_help = "Name of the World. It's hash will be used as a salt when deploying the \ + contract to avoid address conflicts. If not provided root package's name \ + will be used.")] + name: Option, + + #[command(flatten)] + world: WorldOptions, + + #[command(flatten)] + starknet: StarknetOptions, + + #[command(flatten)] + account: AccountOptions, } #[derive(Debug, Subcommand)] pub enum MigrateCommand { #[command(about = "Plan the migration and output the manifests.")] - Plan { - #[arg(long)] - #[arg(help = "Name of the World.")] - #[arg(long_help = "Name of the World. It's hash will be used as a salt when deploying \ - the contract to avoid address conflicts.")] - name: Option, - - #[command(flatten)] - world: WorldOptions, - - #[command(flatten)] - starknet: StarknetOptions, - - #[command(flatten)] - account: AccountOptions, - }, + Plan, #[command(about = "Apply the migration on-chain.")] Apply { - #[arg(long)] - #[arg(help = "Name of the World.")] - #[arg(long_help = "Name of the World. It's hash will be used as a salt when deploying \ - the contract to avoid address conflicts.")] - name: Option, - - #[command(flatten)] - world: WorldOptions, - - #[command(flatten)] - starknet: StarknetOptions, - - #[command(flatten)] - account: AccountOptions, - #[command(flatten)] transaction: TransactionOptions, }, @@ -80,71 +66,35 @@ impl MigrateArgs { return Err(anyhow!("Build project using `sozo build` first")); } + let MigrateArgs { name, world, starknet, account, .. } = self; + + let name = name.unwrap_or_else(|| { + ws.root_package().expect("Root package to be present").id.name.to_string() + }); + + let (world_address, account, rpc_url) = config.tokio_handle().block_on(async { + setup_env(&ws, account, starknet, world, &name, env_metadata.as_ref()).await + })?; + match self.command { - MigrateCommand::Plan { mut name, world, starknet, account } => { - if name.is_none() { - if let Some(root_package) = ws.root_package() { - name = Some(root_package.id.name.to_string()) - } - }; - - config.tokio_handle().block_on(async { - let (world_address, account, chain_id, rpc_url) = setup_env( - &ws, - account, - starknet, - world, - name.as_ref(), - env_metadata.as_ref(), - ) - .await?; - - migration::migrate( - &ws, - world_address, - chain_id, - rpc_url, - &account, - name, - true, - TxnConfig::default(), - ) - .await - }) - } - MigrateCommand::Apply { mut name, world, starknet, account, transaction } => { + MigrateCommand::Plan => config.tokio_handle().block_on(async { + migration::migrate( + &ws, + world_address, + rpc_url, + &account, + &name, + true, + TxnConfig::default(), + ) + .await + }), + MigrateCommand::Apply { transaction } => config.tokio_handle().block_on(async { let txn_config: TxnConfig = transaction.into(); - if name.is_none() { - if let Some(root_package) = ws.root_package() { - name = Some(root_package.id.name.to_string()) - } - }; - - config.tokio_handle().block_on(async { - let (world_address, account, chain_id, rpc_url) = setup_env( - &ws, - account, - starknet, - world, - name.as_ref(), - env_metadata.as_ref(), - ) - .await?; - - migration::migrate( - &ws, - world_address, - chain_id, - rpc_url, - &account, - name, - false, - txn_config, - ) + migration::migrate(&ws, world_address, rpc_url, &account, &name, false, txn_config) .await - }) - } + }), } } } @@ -154,19 +104,18 @@ pub async fn setup_env<'a>( account: AccountOptions, starknet: StarknetOptions, world: WorldOptions, - name: Option<&'a String>, + name: &str, env: Option<&'a Environment>, ) -> Result<( Option, SingleOwnerAccount, LocalWallet>, String, - String, )> { let ui = ws.config().ui(); let world_address = world.address(env).ok(); - let (account, chain_id, rpc_url) = { + let (account, rpc_url) = { let provider = starknet.provider(env)?; let spec_version = provider.spec_version().await?; @@ -191,12 +140,13 @@ pub async fn setup_env<'a>( let address = account.address(); ui.print(format!("\nMigration account: {address:#x}")); - if let Some(name) = name { - ui.print(format!("\nWorld name: {name}\n")); - } + + ui.print(format!("\nWorld name: {name}")); + + ui.print(format!("\nChain ID: {chain_id}\n")); match account.provider().get_class_hash_at(BlockId::Tag(BlockTag::Pending), address).await { - Ok(_) => Ok((account, chain_id, rpc_url)), + Ok(_) => Ok((account, rpc_url)), Err(ProviderError::StarknetError(StarknetError::ContractNotFound)) => { Err(anyhow!("Account with address {:#x} doesn't exist.", account.address())) } @@ -205,5 +155,5 @@ pub async fn setup_env<'a>( } .with_context(|| "Problem initializing account for migration.")?; - Ok((world_address, account, chain_id, rpc_url.to_string())) + Ok((world_address, account, rpc_url.to_string())) } diff --git a/bin/sozo/tests/register_test.rs b/bin/sozo/tests/register_test.rs index e44ac2e96b..607b4634bd 100644 --- a/bin/sozo/tests/register_test.rs +++ b/bin/sozo/tests/register_test.rs @@ -20,7 +20,7 @@ async fn reregister_models() { let base_dir = "../../examples/spawn-and-move"; let target_dir = format!("{}/target/dev", base_dir); - let mut migration = prepare_migration(base_dir.into(), target_dir.into()).unwrap(); + let migration = prepare_migration(base_dir.into(), target_dir.into()).unwrap(); let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; @@ -28,7 +28,7 @@ async fn reregister_models() { let mut account = sequencer.account(); account.set_block_id(BlockId::Tag(BlockTag::Pending)); - execute_strategy(&ws, &mut migration, &account, TxnConfig::default()).await.unwrap(); + execute_strategy(&ws, &migration, &account, TxnConfig::default()).await.unwrap(); let world_address = &format!("0x{:x}", &migration.world_address().unwrap()); let account_address = &format!("0x{:x}", account.address()); let private_key = &format!("0x{:x}", sequencer.raw_account().private_key); @@ -36,7 +36,7 @@ async fn reregister_models() { let moves_model = migration.models.iter().find(|m| m.diff.name == "dojo_examples::models::moves").unwrap(); - let moves_model_class_hash = &format!("0x{:x}", moves_model.diff.local); + let moves_model_class_hash = &format!("0x{:x}", moves_model.diff.local_class_hash); let args_vec = [ "register", "model", diff --git a/crates/dojo-test-utils/src/migration.rs b/crates/dojo-test-utils/src/migration.rs index 36ded5bde6..d2434ca761 100644 --- a/crates/dojo-test-utils/src/migration.rs +++ b/crates/dojo-test-utils/src/migration.rs @@ -4,6 +4,8 @@ use dojo_lang::compiler::{BASE_DIR, MANIFESTS_DIR}; use dojo_world::manifest::BaseManifest; use dojo_world::migration::strategy::{prepare_for_migration, MigrationStrategy}; use dojo_world::migration::world::WorldDiff; +use katana_primitives::FieldElement; +use starknet::core::utils::cairo_short_string_to_felt; use starknet::macros::felt; pub fn prepare_migration( @@ -20,5 +22,25 @@ pub fn prepare_migration( let world = WorldDiff::compute(manifest, None); - prepare_for_migration(None, Some(felt!("0x12345")), &target_dir, world) + prepare_for_migration(None, felt!("0x12345"), &target_dir, world) +} + +pub fn prepare_migration_with_world_and_seed( + manifest_dir: Utf8PathBuf, + target_dir: Utf8PathBuf, + world_address: Option, + seed: &str, +) -> Result { + // In testing, profile name is always dev. + let profile_name = "dev"; + + let manifest = BaseManifest::load_from_path( + &manifest_dir.join(MANIFESTS_DIR).join(profile_name).join(BASE_DIR), + ) + .unwrap(); + + let world = WorldDiff::compute(manifest, None); + + let seed = cairo_short_string_to_felt(seed).unwrap(); + prepare_for_migration(world_address, seed, &target_dir, world) } diff --git a/crates/dojo-world/src/contracts/world_test.rs b/crates/dojo-world/src/contracts/world_test.rs index 3f14e50e73..2edf50cf6e 100644 --- a/crates/dojo-world/src/contracts/world_test.rs +++ b/crates/dojo-world/src/contracts/world_test.rs @@ -47,7 +47,7 @@ pub async fn deploy_world( let strategy = prepare_for_migration( None, - Some(FieldElement::from_hex_be("0x12345").unwrap()), + FieldElement::from_hex_be("0x12345").unwrap(), target_dir, world, ) diff --git a/crates/dojo-world/src/manifest/types.rs b/crates/dojo-world/src/manifest/types.rs index 1ef454c202..afc6f13406 100644 --- a/crates/dojo-world/src/manifest/types.rs +++ b/crates/dojo-world/src/manifest/types.rs @@ -128,7 +128,7 @@ pub struct WorldContract { #[serde_as(as = "Option")] pub transaction_hash: Option, pub block_number: Option, - pub seed: Option, + pub seed: String, pub metadata: Option, } diff --git a/crates/dojo-world/src/migration/class.rs b/crates/dojo-world/src/migration/class.rs index d3f36fb096..7f2376694f 100644 --- a/crates/dojo-world/src/migration/class.rs +++ b/crates/dojo-world/src/migration/class.rs @@ -10,23 +10,27 @@ use super::{Declarable, MigrationType, StateDiff}; #[derive(Debug, Default, Clone)] pub struct ClassDiff { pub name: String, - pub local: FieldElement, - pub original: FieldElement, - pub remote: Option, + pub local_class_hash: FieldElement, + pub original_class_hash: FieldElement, + pub remote_class_hash: Option, } impl StateDiff for ClassDiff { 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 ClassDiff { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { writeln!(f, "{}:", self.name)?; - writeln!(f, " Local: {:#x}", self.local)?; + writeln!(f, " Local: {:#x}", self.local_class_hash)?; - if let Some(remote) = self.remote { + if let Some(remote) = self.remote_class_hash { writeln!(f, " Remote: {remote:#x}")?; } @@ -42,11 +46,11 @@ pub struct ClassMigration { impl ClassMigration { 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, } diff --git a/crates/dojo-world/src/migration/strategy.rs b/crates/dojo-world/src/migration/strategy.rs index 6925a54ede..fae462a248 100644 --- a/crates/dojo-world/src/migration/strategy.rs +++ b/crates/dojo-world/src/migration/strategy.rs @@ -2,7 +2,7 @@ use std::collections::HashMap; use std::fs; use std::path::PathBuf; -use anyhow::{anyhow, Context, Result}; +use anyhow::{anyhow, bail, Context, Result}; use camino::Utf8PathBuf; use starknet::core::types::FieldElement; use starknet::core::utils::{cairo_short_string_to_felt, get_contract_address}; @@ -65,7 +65,7 @@ impl MigrationStrategy { /// evaluate which contracts/classes need to be declared/deployed pub fn prepare_for_migration( world_address: Option, - seed: Option, + seed: FieldElement, target_dir: &Utf8PathBuf, diff: WorldDiff, ) -> Result { @@ -98,16 +98,26 @@ pub fn prepare_for_migration( // If world needs to be migrated, then we expect the `seed` to be provided. if let Some(world) = &mut world { - let salt = - seed.map(poseidon_hash_single).ok_or(anyhow!("Missing seed for World deployment."))?; + let salt = poseidon_hash_single(seed); world.salt = salt; - world.contract_address = get_contract_address( + let generated_world_address = get_contract_address( salt, diff.world.original_class_hash, - &[base.as_ref().unwrap().diff.original], + &[base.as_ref().unwrap().diff.original_class_hash], FieldElement::ZERO, ); + + if let Some(world_address) = world_address { + if world_address != generated_world_address { + bail!( + "Calculated world address doesn't match provided world address.\nIf you are \ + deploying with custom seed make sure `world_address` is correctly configured \ + (or not set) `Scarb.toml`" + ) + } + } + world.contract_address = generated_world_address; } Ok(MigrationStrategy { world_address, world, base, contracts, models }) @@ -136,8 +146,10 @@ fn evaluate_class_to_migrate( artifact_paths: &HashMap, world_contract_will_migrate: bool, ) -> Result> { - match class.remote { - Some(remote) if remote == class.local && !world_contract_will_migrate => Ok(None), + match class.remote_class_hash { + Some(remote) if remote == class.local_class_hash && !world_contract_will_migrate => { + Ok(None) + } _ => { let path = find_artifact_path(class.name.as_str(), artifact_paths)?; Ok(Some(ClassMigration { diff: class.clone(), artifact_path: path.clone() })) diff --git a/crates/dojo-world/src/migration/world.rs b/crates/dojo-world/src/migration/world.rs index b8a508f6e3..7fdad6e43d 100644 --- a/crates/dojo-world/src/migration/world.rs +++ b/crates/dojo-world/src/migration/world.rs @@ -30,9 +30,9 @@ impl WorldDiff { .iter() .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| { + local_class_hash: *model.inner.class_hash(), + original_class_hash: *model.inner.original_class_hash(), + remote_class_hash: remote.as_ref().and_then(|m| { // Remote models are detected from events, where only the struct // name (pascal case) is emitted. // Local models uses the fully qualified name of the model, @@ -80,9 +80,9 @@ impl WorldDiff { 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()), + local_class_hash: *local.base.inner.class_hash(), + original_class_hash: *local.base.inner.original_class_hash(), + remote_class_hash: remote.as_ref().map(|m| *m.base.inner.class_hash()), }; let world = ContractDiff { diff --git a/crates/sozo/ops/src/migration/migrate.rs b/crates/sozo/ops/src/migration/migrate.rs new file mode 100644 index 0000000000..1dd49310c4 --- /dev/null +++ b/crates/sozo/ops/src/migration/migrate.rs @@ -0,0 +1,867 @@ +use std::path::Path; + +use anyhow::{anyhow, bail, Context, Result}; +use camino::Utf8PathBuf; +use dojo_lang::compiler::{ABIS_DIR, BASE_DIR, DEPLOYMENTS_DIR, MANIFESTS_DIR}; +use dojo_world::contracts::abi::world; +use dojo_world::contracts::{cairo_utils, WorldContract}; +use dojo_world::manifest::{ + AbiFormat, BaseManifest, DeploymentManifest, DojoContract, DojoModel, Manifest, + ManifestMethods, WorldContract as ManifestWorldContract, WorldMetadata, +}; +use dojo_world::metadata::{dojo_metadata_from_workspace, ArtifactMetadata}; +use dojo_world::migration::class::ClassMigration; +use dojo_world::migration::contract::ContractMigration; +use dojo_world::migration::strategy::{generate_salt, prepare_for_migration, MigrationStrategy}; +use dojo_world::migration::world::WorldDiff; +use dojo_world::migration::{ + Declarable, Deployable, MigrationError, RegisterOutput, TxnConfig, Upgradable, +}; +use dojo_world::utils::{TransactionExt, TransactionWaiter}; +use futures::future; +use scarb::core::Workspace; +use scarb_ui::Ui; +use starknet::accounts::{Account, ConnectedAccount, SingleOwnerAccount}; +use starknet::core::types::{ + BlockId, BlockTag, FunctionCall, InvokeTransactionResult, StarknetError, +}; +use starknet::core::utils::{ + cairo_short_string_to_felt, get_contract_address, get_selector_from_name, +}; +use starknet::providers::{Provider, ProviderError}; +use starknet::signers::Signer; +use starknet_crypto::FieldElement; +use tokio::fs; + +use super::ui::{bold_message, italic_message, MigrationUi}; +use super::{ + ContractDeploymentOutput, ContractMigrationOutput, ContractUpgradeOutput, MigrationOutput, +}; + +pub fn prepare_migration( + target_dir: &Utf8PathBuf, + diff: WorldDiff, + name: &str, + world_address: Option, + ui: &Ui, +) -> Result { + ui.print_step(3, "šŸ“¦", "Preparing for migration..."); + + let name = cairo_short_string_to_felt(name).with_context(|| "Failed to parse World name.")?; + + let migration = prepare_for_migration(world_address, name, target_dir, diff) + .with_context(|| "Problem preparing for migration.")?; + + let info = migration.info(); + + ui.print_sub(format!( + "Total items to be migrated ({}): New {} Update {}", + info.new + info.update, + info.new, + info.update + )); + + Ok(migration) +} + +pub async fn apply_diff( + ws: &Workspace<'_>, + account: &SingleOwnerAccount, + txn_config: TxnConfig, + strategy: &mut MigrationStrategy, +) -> Result +where + P: Provider + Sync + Send + 'static, + S: Signer + Sync + Send + 'static, +{ + let ui = ws.config().ui(); + + ui.print_step(4, "šŸ› ", "Migrating..."); + ui.print(" "); + + let migration_output = execute_strategy(ws, strategy, account, txn_config) + .await + .map_err(|e| anyhow!(e)) + .with_context(|| "Problem trying to migrate.")?; + + if migration_output.full { + if let Some(block_number) = migration_output.world_block_number { + 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") + )) + )); + } else { + ui.print(format!( + "\nšŸŽ‰ Successfully migrated World at address {}\n", + bold_message(format!( + "{:#x}", + strategy.world_address().expect("world address must exist") + )) + )); + } + } else { + ui.print(format!( + "\nšŸšØ Partially migrated World at address {}", + bold_message(format!( + "{:#x}", + strategy.world_address().expect("world address must exist") + )) + )); + } + + Ok(migration_output) +} + +pub async fn execute_strategy( + ws: &Workspace<'_>, + strategy: &MigrationStrategy, + migrator: &SingleOwnerAccount, + txn_config: TxnConfig, +) -> Result +where + P: Provider + Sync + Send + 'static, + S: Signer + Sync + Send + 'static, +{ + let ui = ws.config().ui(); + let mut world_tx_hash: Option = None; + let mut world_block_number: Option = None; + + match &strategy.base { + Some(base) => { + ui.print_header("# Base Contract"); + + match base.declare(migrator, &txn_config).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_class_hash)); + } + Err(MigrationError::ArtifactError(e)) => { + return Err(handle_artifact_error(&ui, base.artifact_path(), e)); + } + Err(e) => { + ui.verbose(format!("{e:?}")); + return Err(e.into()); + } + }; + } + None => {} + }; + + match &strategy.world { + Some(world) => { + ui.print_header("# World"); + + // 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_class_hash, + migrator, + &ui, + &txn_config, + ) + .await + .map_err(|e| { + ui.verbose(format!("{e:?}")); + anyhow!("Failed to upgrade world: {e}") + })?; + + ui.print_sub(format!( + "Upgraded Contract at address: {:#x}", + world.contract_address + )); + } else { + let calldata = vec![strategy.base.as_ref().unwrap().diff.local_class_hash]; + 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)); + } + } + None => {} + }; + + let mut migration_output = MigrationOutput { + world_address: strategy.world_address()?, + world_tx_hash, + world_block_number, + full: false, + models: vec![], + contracts: vec![], + }; + + let world_address = strategy.world_address()?; + + // Once Torii supports indexing arrays, we should declare and register the + // ResourceMetadata model. + match register_dojo_models(&strategy.models, world_address, migrator, &ui, &txn_config).await { + Ok(output) => { + migration_output.models = output.registered_model_names; + } + Err(e) => { + ui.anyhow(&e); + return Ok(migration_output); + } + }; + + match register_dojo_contracts(&strategy.contracts, world_address, migrator, &ui, &txn_config) + .await + { + Ok(output) => { + migration_output.contracts = output; + } + Err(e) => { + ui.anyhow(&e); + return Ok(migration_output); + } + }; + + migration_output.full = true; + + Ok(migration_output) +} + +/// Upload a metadata as a IPFS artifact and then create a resource to register +/// 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. +/// +/// # Returns +/// A [`ResourceData`] object to register in the Dojo resource register +/// on success. +async fn upload_on_ipfs_and_create_resource( + ui: &Ui, + element_name: String, + resource_id: FieldElement, + artifact: ArtifactMetadata, +) -> Result { + match artifact.upload().await { + Ok(hash) => { + ui.print_sub(format!("{}: ipfs://{}", element_name, hash)); + create_resource_metadata(resource_id, hash) + } + Err(_) => Err(anyhow!("Failed to upload IPFS resource.")), + } +} + +/// Create a resource to register in the Dojo resource registry. +/// +/// # Arguments +/// * `resource_id` - the ID of the resource +/// * `hash` - the IPFS hash +/// +/// # Returns +/// A [`ResourceData`] object to register in the Dojo resource register +/// on success. +fn create_resource_metadata( + resource_id: FieldElement, + hash: String, +) -> Result { + 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()]); + } + + Ok(world::ResourceMetadata { resource_id, metadata_uri: encoded_uri }) +} + +/// Upload metadata of the world/models/contracts as IPFS artifacts and then +/// register them in the Dojo resource registry. +/// +/// # Arguments +/// +/// * `ws` - the workspace +/// * `migrator` - the account used to migrate +/// * `migration_output` - the output after having applied the migration plan. +pub async fn upload_metadata( + ws: &Workspace<'_>, + migrator: &SingleOwnerAccount, + migration_output: MigrationOutput, + txn_config: TxnConfig, +) -> Result<()> +where + P: Provider + Sync + Send + 'static, + S: Signer + Sync + Send + 'static, +{ + let ui = ws.config().ui(); + + ui.print(" "); + ui.print_step(6, "šŸŒ", "Uploading metadata..."); + ui.print(" "); + + let dojo_metadata = dojo_metadata_from_workspace(ws); + let mut ipfs = vec![]; + let mut resources = vec![]; + + // world + if migration_output.world_tx_hash.is_some() { + match dojo_metadata.world.upload().await { + Ok(hash) => { + let resource = create_resource_metadata(FieldElement::ZERO, hash.clone())?; + ui.print_sub(format!("world: ipfs://{}", hash)); + resources.push(resource); + } + Err(err) => { + ui.print_sub(format!("Failed to upload World metadata:\n{err}")); + } + } + } + + // models + if !migration_output.models.is_empty() { + for model_name in migration_output.models { + if let Some(m) = dojo_metadata.artifacts.get(&model_name) { + ipfs.push(upload_on_ipfs_and_create_resource( + &ui, + model_name.clone(), + get_selector_from_name(&model_name).expect("ASCII model name"), + m.clone(), + )); + } + } + } + + // contracts + let migrated_contracts = migration_output.contracts.into_iter().flatten().collect::>(); + + if !migrated_contracts.is_empty() { + for contract in migrated_contracts { + if let Some(m) = dojo_metadata.artifacts.get(&contract.name) { + ipfs.push(upload_on_ipfs_and_create_resource( + &ui, + contract.name.clone(), + contract.contract_address, + m.clone(), + )); + } + } + } + + // upload IPFS + resources.extend( + future::try_join_all(ipfs) + .await + .map_err(|_| anyhow!("Unable to upload IPFS artifacts."))?, + ); + + ui.print("> All IPFS artifacts have been successfully uploaded.".to_string()); + + // update the resource registry + let world = WorldContract::new(migration_output.world_address, migrator); + + let calls = resources.iter().map(|r| world.set_metadata_getcall(r)).collect::>(); + + let InvokeTransactionResult { transaction_hash } = + migrator.execute(calls).send_with_cfg(&txn_config).await.map_err(|e| { + ui.verbose(format!("{e:?}")); + anyhow!("Failed to register metadata into the resource registry: {e}") + })?; + + TransactionWaiter::new(transaction_hash, migrator.provider()).await?; + + ui.print(format!( + "> All metadata have been registered in the resource registry (tx hash: \ + {transaction_hash:#x})" + )); + + ui.print(""); + ui.print("\nāœØ Done."); + + Ok(()) +} + +async fn register_dojo_models( + models: &[ClassMigration], + world_address: FieldElement, + migrator: &SingleOwnerAccount, + ui: &Ui, + txn_config: &TxnConfig, +) -> Result +where + P: Provider + Sync + Send + 'static, + S: Signer + Sync + Send + 'static, +{ + if models.is_empty() { + return Ok(RegisterOutput { + transaction_hash: FieldElement::ZERO, + declare_output: vec![], + registered_model_names: vec![], + }); + } + + ui.print_header(format!("# Models ({})", models.len())); + + let mut declare_output = vec![]; + let mut registered_model_names = vec![]; + + for c in models.iter() { + ui.print(italic_message(&c.diff.name).to_string()); + + let res = c.declare(migrator, txn_config).await; + match res { + Ok(output) => { + ui.print_hidden_sub(format!("Declare transaction: {:#x}", output.transaction_hash)); + + declare_output.push(output); + } + + // Continue if model is already declared + Err(MigrationError::ClassAlreadyDeclared) => { + ui.print_sub(format!("Already declared: {:#x}", c.diff.local_class_hash)); + continue; + } + Err(MigrationError::ArtifactError(e)) => { + return Err(handle_artifact_error(ui, c.artifact_path(), e)); + } + Err(e) => { + ui.verbose(format!("{e:?}")); + bail!("Failed to declare model {}: {e}", c.diff.name) + } + } + + ui.print_sub(format!("Class hash: {:#x}", c.diff.local_class_hash)); + } + + let world = WorldContract::new(world_address, migrator); + + let calls = models + .iter() + .map(|c| { + registered_model_names.push(c.diff.name.clone()); + world.register_model_getcall(&c.diff.local_class_hash.into()) + }) + .collect::>(); + + let InvokeTransactionResult { transaction_hash } = + world.account.execute(calls).send_with_cfg(txn_config).await.map_err(|e| { + ui.verbose(format!("{e:?}")); + anyhow!("Failed to register models to World: {e}") + })?; + + TransactionWaiter::new(transaction_hash, migrator.provider()).await?; + + ui.print(format!("All models are registered at: {transaction_hash:#x}")); + + Ok(RegisterOutput { transaction_hash, declare_output, registered_model_names }) +} + +async fn register_dojo_contracts( + contracts: &Vec, + world_address: FieldElement, + migrator: &SingleOwnerAccount, + ui: &Ui, + txn_config: &TxnConfig, +) -> Result>> +where + P: Provider + Sync + Send + 'static, + S: Signer + Sync + Send + 'static, +{ + if contracts.is_empty() { + return Ok(vec![]); + } + + ui.print_header(format!("# Contracts ({})", contracts.len())); + + let mut deploy_output = vec![]; + + for contract in contracts { + let name = &contract.diff.name; + ui.print(italic_message(name).to_string()); + match contract + .deploy_dojo_contract( + world_address, + contract.diff.local_class_hash, + contract.diff.base_class_hash, + migrator, + txn_config, + ) + .await + { + Ok(output) => { + if let Some(ref declare) = output.declare { + ui.print_hidden_sub(format!( + "Declare transaction: {:#x}", + declare.transaction_hash + )); + } + + // NOTE: this assignment may not look useful since we are dropping + // `MigrationStrategy` without actually using this value from it. + // but some tests depend on this behaviour + // contract.contract_address = 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(ContractMigrationOutput { + name: name.to_string(), + contract_address: output.contract_address, + base_class_hash: output.base_class_hash, + })); + } + Err(MigrationError::ContractAlreadyDeployed(contract_address)) => { + ui.print_sub(format!("Already deployed: {:#x}", contract_address)); + deploy_output.push(None); + } + Err(MigrationError::ArtifactError(e)) => { + return Err(handle_artifact_error(ui, contract.artifact_path(), e)); + } + Err(e) => { + ui.verbose(format!("{e:?}")); + return Err(anyhow!("Failed to migrate {name}: {e}")); + } + } + } + + Ok(deploy_output) +} + +async fn deploy_contract( + contract: &ContractMigration, + contract_id: &str, + constructor_calldata: Vec, + migrator: &SingleOwnerAccount, + ui: &Ui, + txn_config: &TxnConfig, +) -> Result +where + P: Provider + Sync + Send + 'static, + S: Signer + Sync + Send + 'static, +{ + match contract + .deploy(contract.diff.local_class_hash, constructor_calldata, migrator, txn_config) + .await + { + Ok(mut val) => { + if let Some(declare) = val.clone().declare { + ui.print_hidden_sub(format!( + "Declare transaction: {:#x}", + declare.transaction_hash + )); + } + + ui.print_hidden_sub(format!("Deploy transaction: {:#x}", val.transaction_hash)); + + val.name = Some(contract.diff.name.clone()); + Ok(ContractDeploymentOutput::Output(val)) + } + Err(MigrationError::ContractAlreadyDeployed(contract_address)) => { + Ok(ContractDeploymentOutput::AlreadyDeployed(contract_address)) + } + Err(MigrationError::ArtifactError(e)) => { + return Err(handle_artifact_error(ui, contract.artifact_path(), e)); + } + Err(e) => { + ui.verbose(format!("{e:?}")); + Err(anyhow!("Failed to migrate {contract_id}: {e}")) + } + } +} + +async fn upgrade_contract( + contract: &ContractMigration, + contract_id: &str, + original_class_hash: FieldElement, + original_base_class_hash: FieldElement, + migrator: &SingleOwnerAccount, + ui: &Ui, + txn_config: &TxnConfig, +) -> 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, + ) + .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}")) + } + } +} + +pub fn handle_artifact_error(ui: &Ui, artifact_path: &Path, error: anyhow::Error) -> anyhow::Error { + let path = artifact_path.to_string_lossy(); + let name = artifact_path.file_name().unwrap().to_string_lossy(); + ui.verbose(format!("{path}: {error:?}")); + + anyhow!( + "Discrepancy detected in {name}.\nUse `sozo clean` to clean your project or `sozo clean \ + --artifacts` to clean artifacts only.\nThen, rebuild your project with `sozo build`." + ) +} + +pub async fn get_contract_operation_name

( + provider: &P, + contract: &ContractMigration, + world_address: Option, +) -> String +where + P: Provider + Sync + Send + 'static, +{ + 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); + + 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.name); + } + Err(ProviderError::StarknetError(StarknetError::ContractNotFound)) => { + return format!("deploy {}", contract.diff.name); + } + Ok(_) => return "already deployed".to_string(), + Err(_) => return format!("deploy {}", contract.diff.name), + } + } + } + format!("deploy {}", contract.diff.name) +} + +pub async fn print_strategy

(ui: &Ui, provider: &P, strategy: &MigrationStrategy) +where + P: Provider + Sync + Send + 'static, +{ + ui.print("\nšŸ“‹ Migration Strategy\n"); + + if let Some(base) = &strategy.base { + ui.print_header("# Base Contract"); + ui.print_sub(format!("declare (class hash: {:#x})\n", base.diff.local_class_hash)); + } + + if let Some(world) = &strategy.world { + ui.print_header("# World"); + ui.print_sub(format!("declare (class hash: {:#x})\n", world.diff.local_class_hash)); + } + + if !&strategy.models.is_empty() { + ui.print_header(format!("# Models ({})", &strategy.models.len())); + for m in &strategy.models { + ui.print_sub(format!( + "register {} (class hash: {:#x})", + m.diff.name, m.diff.local_class_hash + )); + } + ui.print(" "); + } + + if !&strategy.contracts.is_empty() { + 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_class_hash)); + } + ui.print(" "); + } +} + +#[allow(clippy::too_many_arguments)] +pub async fn update_manifests_and_abis( + ws: &Workspace<'_>, + local_manifest: BaseManifest, + profile_dir: &Utf8PathBuf, + profile_name: &str, + rpc_url: &str, + world_address: FieldElement, + migration_output: Option, + salt: &str, +) -> Result<()> { + let ui = ws.config().ui(); + ui.print_step(5, "āœØ", "Updating manifests..."); + + let deployed_path = profile_dir.join("manifest").with_extension("toml"); + let deployed_path_json = profile_dir.join("manifest").with_extension("json"); + + let mut local_manifest: DeploymentManifest = local_manifest.into(); + + local_manifest.world.inner.metadata = Some(WorldMetadata { + profile_name: profile_name.to_string(), + rpc_url: rpc_url.to_string(), + }); + + if deployed_path.exists() { + let previous_manifest = DeploymentManifest::load_from_path(&deployed_path)?; + local_manifest.merge_from_previous(previous_manifest); + }; + + local_manifest.world.inner.address = Some(world_address); + local_manifest.world.inner.seed = salt.to_owned(); + + // 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 { + if migration_output.world_tx_hash.is_some() { + local_manifest.world.inner.transaction_hash = migration_output.world_tx_hash; + } + if migration_output.world_block_number.is_some() { + local_manifest.world.inner.block_number = migration_output.world_block_number; + } + + 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 + let local = local_manifest + .contracts + .iter_mut() + .find(|c| c.name == output.name) + .expect("contract got migrated, means it should be present here"); + + local.inner.base_class_hash = output.base_class_hash; + } + }); + } + + local_manifest.contracts.iter_mut().for_each(|contract| { + let salt = generate_salt(&contract.name); + contract.inner.address = + Some(get_contract_address(salt, contract.inner.base_class_hash, &[], world_address)); + }); + + // copy abi files from `abi/base` to `abi/deployments/{chain_id}` and update abi path in + // local_manifest + update_manifest_abis(&mut local_manifest, profile_dir, profile_name).await; + + local_manifest.write_to_path_toml(&deployed_path)?; + local_manifest.write_to_path_json(&deployed_path_json, profile_dir)?; + ui.print("\nāœØ Done."); + + Ok(()) +} + +async fn update_manifest_abis( + local_manifest: &mut DeploymentManifest, + profile_dir: &Utf8PathBuf, + profile_name: &str, +) { + fs::create_dir_all(profile_dir.join(ABIS_DIR).join(DEPLOYMENTS_DIR)) + .await + .expect("Failed to create folder"); + + async fn inner_helper( + profile_dir: &Utf8PathBuf, + profile_name: &str, + manifest: &mut Manifest, + ) where + T: ManifestMethods, + { + // Unwraps in call to abi is safe because we always write abis for DojoContracts as relative + // path. + // In this relative path, we only what the root from + // ABI directory. + let base_relative_path = manifest + .inner + .abi() + .unwrap() + .to_path() + .unwrap() + .strip_prefix(Utf8PathBuf::new().join(MANIFESTS_DIR).join(profile_name)) + .unwrap(); + + // The filename is safe to unwrap as it's always + // present in the base relative path. + let deployed_relative_path = Utf8PathBuf::new().join(ABIS_DIR).join(DEPLOYMENTS_DIR).join( + base_relative_path + .strip_prefix(Utf8PathBuf::new().join(ABIS_DIR).join(BASE_DIR)) + .unwrap(), + ); + + let full_base_path = profile_dir.join(base_relative_path); + let full_deployed_path = profile_dir.join(deployed_relative_path.clone()); + + fs::create_dir_all(full_deployed_path.parent().unwrap()) + .await + .expect("Failed to create folder"); + + fs::copy(full_base_path, full_deployed_path).await.expect("Failed to copy abi file"); + + manifest.inner.set_abi(Some(AbiFormat::Path(deployed_relative_path))); + } + + inner_helper::(profile_dir, profile_name, &mut local_manifest.world) + .await; + + for contract in local_manifest.contracts.iter_mut() { + inner_helper::(profile_dir, profile_name, contract).await; + } + + for model in local_manifest.models.iter_mut() { + inner_helper::(profile_dir, profile_name, model).await; + } +} diff --git a/crates/sozo/ops/src/migration/migration_test.rs b/crates/sozo/ops/src/migration/migration_test.rs deleted file mode 100644 index 1ba389d6e1..0000000000 --- a/crates/sozo/ops/src/migration/migration_test.rs +++ /dev/null @@ -1,174 +0,0 @@ -use camino::Utf8Path; -use dojo_lang::compiler::{BASE_DIR, MANIFESTS_DIR}; -use dojo_test_utils::compiler::build_test_config; -use dojo_test_utils::migration::prepare_migration; -use dojo_test_utils::sequencer::{ - get_default_test_starknet_config, SequencerConfig, StarknetConfig, TestSequencer, -}; -use dojo_world::manifest::{BaseManifest, DeploymentManifest}; -use dojo_world::migration::strategy::prepare_for_migration; -use dojo_world::migration::world::WorldDiff; -use dojo_world::migration::TxnConfig; -use scarb::ops; -use starknet::accounts::{ExecutionEncoding, SingleOwnerAccount}; -use starknet::core::chain_id; -use starknet::core::types::{BlockId, BlockTag}; -use starknet::macros::felt; -use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::JsonRpcClient; -use starknet::signers::{LocalWallet, SigningKey}; - -use crate::migration::execute_strategy; - -#[tokio::test(flavor = "multi_thread")] -async fn migrate_with_auto_mine() { - let config = build_test_config("../../../examples/spawn-and-move/Scarb.toml").unwrap(); - let ws = ops::read_workspace(config.manifest_path(), &config) - .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); - - let base_dir = "../../../examples/spawn-and-move"; - let target_dir = format!("{}/target/dev", base_dir); - let mut migration = prepare_migration(base_dir.into(), target_dir.into()).unwrap(); - - let sequencer = - TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; - - let mut account = sequencer.account(); - account.set_block_id(BlockId::Tag(BlockTag::Pending)); - - execute_strategy(&ws, &mut migration, &account, None).await.unwrap(); - - sequencer.stop().unwrap(); -} - -#[tokio::test(flavor = "multi_thread")] -async fn migrate_with_block_time() { - let config = build_test_config("../../../examples/spawn-and-move/Scarb.toml").unwrap(); - let ws = ops::read_workspace(config.manifest_path(), &config) - .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); - - let base = "../../../examples/spawn-and-move"; - let target_dir = format!("{}/target/dev", base); - let mut migration = prepare_migration(base.into(), target_dir.into()).unwrap(); - - let sequencer = TestSequencer::start( - SequencerConfig { block_time: Some(1000), ..Default::default() }, - get_default_test_starknet_config(), - ) - .await; - - let mut account = sequencer.account(); - account.set_block_id(BlockId::Tag(BlockTag::Pending)); - - execute_strategy(&ws, &mut migration, &account, None).await.unwrap(); - sequencer.stop().unwrap(); -} - -#[tokio::test(flavor = "multi_thread")] -async fn migrate_with_small_fee_multiplier_will_fail() { - let config = build_test_config("../../../examples/spawn-and-move/Scarb.toml").unwrap(); - let ws = ops::read_workspace(config.manifest_path(), &config) - .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); - - let base = "../../../examples/spawn-and-move"; - let target_dir = format!("{}/target/dev", base); - let mut migration = prepare_migration(base.into(), target_dir.into()).unwrap(); - - let sequencer = TestSequencer::start( - Default::default(), - StarknetConfig { disable_fee: false, ..Default::default() }, - ) - .await; - - let account = SingleOwnerAccount::new( - JsonRpcClient::new(HttpTransport::new(sequencer.url())), - LocalWallet::from_signing_key(SigningKey::from_secret_scalar( - sequencer.raw_account().private_key, - )), - sequencer.raw_account().account_address, - chain_id::TESTNET, - ExecutionEncoding::New, - ); - - assert!(execute_strategy( - &ws, - &mut migration, - &account, - Some(TxnConfig { fee_estimate_multiplier: Some(0.2f64), wait: false, receipt: false }), - ) - .await - .is_err()); - sequencer.stop().unwrap(); -} - -#[test] -fn migrate_world_without_seed_will_fail() { - let profile_name = "dev"; - let base = "../../../examples/spawn-and-move"; - let target_dir = format!("{}/target/dev", base); - let manifest = BaseManifest::load_from_path( - &Utf8Path::new(base).to_path_buf().join(MANIFESTS_DIR).join(profile_name).join(BASE_DIR), - ) - .unwrap(); - let world = WorldDiff::compute(manifest, None); - let res = prepare_for_migration(None, None, &Utf8Path::new(&target_dir).to_path_buf(), world); - assert!(res.is_err_and(|e| e.to_string().contains("Missing seed for World deployment."))) -} - -#[tokio::test] -async fn migration_from_remote() { - let config = build_test_config("../../../examples/spawn-and-move/Scarb.toml").unwrap(); - let ws = ops::read_workspace(config.manifest_path(), &config) - .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); - let base = "../../../examples/spawn-and-move"; - let target_dir = format!("{}/target/dev", base); - - let sequencer = - TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; - - let account = SingleOwnerAccount::new( - JsonRpcClient::new(HttpTransport::new(sequencer.url())), - LocalWallet::from_signing_key(SigningKey::from_secret_scalar( - sequencer.raw_account().private_key, - )), - sequencer.raw_account().account_address, - chain_id::TESTNET, - ExecutionEncoding::New, - ); - - let profile_name = ws.current_profile().unwrap().to_string(); - - let manifest = BaseManifest::load_from_path( - &Utf8Path::new(base).to_path_buf().join(MANIFESTS_DIR).join(&profile_name).join(BASE_DIR), - ) - .unwrap(); - - let world = WorldDiff::compute(manifest, None); - - let mut migration = prepare_for_migration( - None, - Some(felt!("0x12345")), - &Utf8Path::new(&target_dir).to_path_buf(), - world, - ) - .unwrap(); - - execute_strategy(&ws, &mut migration, &account, None).await.unwrap(); - - let local_manifest = BaseManifest::load_from_path( - &Utf8Path::new(base).to_path_buf().join(MANIFESTS_DIR).join(&profile_name).join(BASE_DIR), - ) - .unwrap(); - - let remote_manifest = DeploymentManifest::load_from_remote( - JsonRpcClient::new(HttpTransport::new(sequencer.url())), - migration.world_address().unwrap(), - ) - .await - .unwrap(); - - sequencer.stop().unwrap(); - - assert_eq!(local_manifest.world.inner.class_hash, remote_manifest.world.inner.class_hash); - assert_eq!(local_manifest.models.len(), remote_manifest.models.len()); -} diff --git a/crates/sozo/ops/src/migration/mod.rs b/crates/sozo/ops/src/migration/mod.rs index 4c4ba4ded2..b193826a06 100644 --- a/crates/sozo/ops/src/migration/mod.rs +++ b/crates/sozo/ops/src/migration/mod.rs @@ -1,44 +1,22 @@ -use std::path::Path; - -use anyhow::{anyhow, bail, Context, Result}; -use camino::Utf8PathBuf; -use dojo_lang::compiler::{ABIS_DIR, BASE_DIR, DEPLOYMENTS_DIR, MANIFESTS_DIR, OVERLAYS_DIR}; -use dojo_world::contracts::abi::world::ResourceMetadata; -use dojo_world::contracts::cairo_utils; -use dojo_world::contracts::world::WorldContract; -use dojo_world::manifest::{ - AbiFormat, AbstractManifestError, BaseManifest, DeploymentManifest, DojoContract, DojoModel, - Manifest, ManifestMethods, OverlayManifest, WorldContract as ManifestWorldContract, - WorldMetadata, -}; -use dojo_world::metadata::{dojo_metadata_from_workspace, ArtifactMetadata}; -use dojo_world::migration::contract::ContractMigration; -use dojo_world::migration::strategy::{generate_salt, prepare_for_migration, MigrationStrategy}; +use anyhow::{anyhow, Result}; +use dojo_lang::compiler::MANIFESTS_DIR; use dojo_world::migration::world::WorldDiff; -use dojo_world::migration::{ - Declarable, DeployOutput, Deployable, MigrationError, RegisterOutput, StateDiff, TxnConfig, - Upgradable, UpgradeOutput, -}; -use dojo_world::utils::{TransactionExt, TransactionWaiter}; -use futures::future; +use dojo_world::migration::{DeployOutput, TxnConfig, UpgradeOutput}; use scarb::core::Workspace; -use scarb_ui::Ui; -use starknet::accounts::{Account, ConnectedAccount, SingleOwnerAccount}; -use starknet::core::types::{ - BlockId, BlockTag, FieldElement, FunctionCall, InvokeTransactionResult, StarknetError, -}; -use starknet::core::utils::{ - cairo_short_string_to_felt, get_contract_address, get_selector_from_name, -}; -use starknet::providers::{Provider, ProviderError}; -use tokio::fs; +use starknet::accounts::{ConnectedAccount, SingleOwnerAccount}; +use starknet::core::types::FieldElement; +use starknet::providers::Provider; +use starknet::signers::Signer; +mod migrate; mod ui; +mod utils; -use starknet::signers::Signer; -use ui::MigrationUi; - -use self::ui::{bold_message, italic_message}; +use self::migrate::update_manifests_and_abis; +pub use self::migrate::{ + apply_diff, execute_strategy, prepare_migration, print_strategy, upload_metadata, +}; +use self::ui::MigrationUi; #[derive(Debug, Default, Clone)] pub struct MigrationOutput { @@ -55,19 +33,18 @@ pub struct MigrationOutput { #[derive(Debug, Default, Clone)] pub struct ContractMigrationOutput { - name: String, - contract_address: FieldElement, - base_class_hash: FieldElement, + pub name: String, + pub contract_address: FieldElement, + pub base_class_hash: FieldElement, } #[allow(clippy::too_many_arguments)] pub async fn migrate( ws: &Workspace<'_>, world_address: Option, - chain_id: String, rpc_url: String, account: &SingleOwnerAccount, - name: Option, + name: &str, dry_run: bool, txn_config: TxnConfig, ) -> Result<()> @@ -77,9 +54,6 @@ where { let ui = ws.config().ui(); - // Setup account for migration and fetch world address if it exists. - ui.print(format!("Chain ID: {}\n", &chain_id)); - // its path to a file so `parent` should never return `None` let manifest_dir = ws.manifest_path().parent().unwrap().to_path_buf(); @@ -92,13 +66,16 @@ where // Load local and remote World manifests. let (local_manifest, remote_manifest) = - load_world_manifests(&profile_dir, account, world_address, &ui).await.map_err(|e| { - ui.error(e.to_string()); - anyhow!( - "\n Use `sozo clean` to clean your project, or `sozo clean --manifests-abis` to \ - clean manifest and abi files only.\nThen, rebuild your project with `sozo build`.", - ) - })?; + utils::load_world_manifests(&profile_dir, account, world_address, &ui).await.map_err( + |e| { + ui.error(e.to_string()); + anyhow!( + "\n Use `sozo clean` to clean your project, or `sozo clean --manifests-abis` \ + to clean manifest and abi files only.\nThen, rebuild your project with `sozo \ + build`.", + ) + }, + )?; // Calculate diff between local and remote World manifests. ui.print_step(2, "šŸ§°", "Evaluating Worlds diff..."); @@ -111,7 +88,7 @@ where return Ok(()); } - let mut strategy = prepare_migration(&target_dir, diff, name.clone(), world_address, &ui)?; + let mut strategy = prepare_migration(&target_dir, diff, name, world_address, &ui)?; let world_address = strategy.world_address().expect("world address must exist"); if dry_run { @@ -125,29 +102,13 @@ where &rpc_url, world_address, None, - name.as_ref(), + name, ) .await?; } else { // Migrate according to the diff. - match apply_diff(ws, account, txn_config, &mut strategy).await { - Ok(migration_output) => { - update_manifests_and_abis( - ws, - local_manifest.clone(), - &profile_dir, - &profile_name, - &rpc_url, - world_address, - Some(migration_output.clone()), - name.as_ref(), - ) - .await?; - - if !ws.config().offline() { - upload_metadata(ws, account, migration_output, txn_config).await?; - } - } + 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, @@ -157,410 +118,33 @@ where &rpc_url, world_address, None, - name.as_ref(), + name, ) .await?; return Err(e)?; } - } - }; - - Ok(()) -} - -#[allow(clippy::too_many_arguments)] -async fn update_manifests_and_abis( - ws: &Workspace<'_>, - local_manifest: BaseManifest, - profile_dir: &Utf8PathBuf, - profile_name: &str, - rpc_url: &str, - world_address: FieldElement, - migration_output: Option, - salt: Option<&String>, -) -> Result<()> { - let ui = ws.config().ui(); - ui.print_step(5, "āœØ", "Updating manifests..."); - - let deployed_path = profile_dir.join("manifest").with_extension("toml"); - let deployed_path_json = profile_dir.join("manifest").with_extension("json"); - - let mut local_manifest: DeploymentManifest = local_manifest.into(); - - local_manifest.world.inner.metadata = Some(WorldMetadata { - profile_name: profile_name.to_string(), - rpc_url: rpc_url.to_string(), - }); - - if deployed_path.exists() { - let previous_manifest = DeploymentManifest::load_from_path(&deployed_path)?; - local_manifest.merge_from_previous(previous_manifest); - }; - - local_manifest.world.inner.address = Some(world_address); - if let Some(salt) = salt { - local_manifest.world.inner.seed = Some(salt.to_owned()); - } - - // 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 { - if migration_output.world_tx_hash.is_some() { - local_manifest.world.inner.transaction_hash = migration_output.world_tx_hash; - } - if migration_output.world_block_number.is_some() { - local_manifest.world.inner.block_number = migration_output.world_block_number; - } - - 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 - let local = local_manifest - .contracts - .iter_mut() - .find(|c| c.name == output.name) - .expect("contract got migrated, means it should be present here"); - - local.inner.base_class_hash = output.base_class_hash; - } - }); - } - - local_manifest.contracts.iter_mut().for_each(|contract| { - let salt = generate_salt(&contract.name); - contract.inner.address = - Some(get_contract_address(salt, contract.inner.base_class_hash, &[], world_address)); - }); - - // copy abi files from `abi/base` to `abi/deployments/{chain_id}` and update abi path in - // local_manifest - update_manifest_abis(&mut local_manifest, profile_dir, profile_name).await; - - local_manifest.write_to_path_toml(&deployed_path)?; - local_manifest.write_to_path_json(&deployed_path_json, profile_dir)?; - ui.print("\nāœØ Done."); - - Ok(()) -} - -async fn update_manifest_abis( - local_manifest: &mut DeploymentManifest, - profile_dir: &Utf8PathBuf, - profile_name: &str, -) { - fs::create_dir_all(profile_dir.join(ABIS_DIR).join(DEPLOYMENTS_DIR)) - .await - .expect("Failed to create folder"); - - async fn inner_helper( - profile_dir: &Utf8PathBuf, - profile_name: &str, - manifest: &mut Manifest, - ) where - T: ManifestMethods, - { - // Unwraps in call to abi is safe because we always write abis for DojoContracts as relative - // path. - // In this relative path, we only what the root from - // ABI directory. - let base_relative_path = manifest - .inner - .abi() - .unwrap() - .to_path() - .unwrap() - .strip_prefix(Utf8PathBuf::new().join(MANIFESTS_DIR).join(profile_name)) - .unwrap(); - - // The filename is safe to unwrap as it's always - // present in the base relative path. - let deployed_relative_path = Utf8PathBuf::new().join(ABIS_DIR).join(DEPLOYMENTS_DIR).join( - base_relative_path - .strip_prefix(Utf8PathBuf::new().join(ABIS_DIR).join(BASE_DIR)) - .unwrap(), - ); - - let full_base_path = profile_dir.join(base_relative_path); - let full_deployed_path = profile_dir.join(deployed_relative_path.clone()); - - fs::create_dir_all(full_deployed_path.parent().unwrap()) - .await - .expect("Failed to create folder"); - - fs::copy(full_base_path, full_deployed_path).await.expect("Failed to copy abi file"); - - manifest.inner.set_abi(Some(AbiFormat::Path(deployed_relative_path))); - } - - inner_helper::(profile_dir, profile_name, &mut local_manifest.world) - .await; - - for contract in local_manifest.contracts.iter_mut() { - inner_helper::(profile_dir, profile_name, contract).await; - } - - for model in local_manifest.models.iter_mut() { - inner_helper::(profile_dir, profile_name, model).await; - } -} + }; -pub async fn apply_diff( - ws: &Workspace<'_>, - account: &SingleOwnerAccount, - txn_config: TxnConfig, - strategy: &mut MigrationStrategy, -) -> Result -where - P: Provider + Sync + Send + 'static, - S: Signer + Sync + Send + 'static, -{ - let ui = ws.config().ui(); - - ui.print_step(4, "šŸ› ", "Migrating..."); - ui.print(" "); - - let migration_output = execute_strategy(ws, strategy, account, txn_config) - .await - .map_err(|e| anyhow!(e)) - .with_context(|| "Problem trying to migrate.")?; - - if migration_output.full { - if let Some(block_number) = migration_output.world_block_number { - 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") - )) - )); - } else { - ui.print(format!( - "\nšŸŽ‰ Successfully migrated World at address {}\n", - bold_message(format!( - "{:#x}", - strategy.world_address().expect("world address must exist") - )) - )); - } - } else { - ui.print(format!( - "\nšŸšØ Partially migrated World at address {}", - bold_message(format!( - "{:#x}", - strategy.world_address().expect("world address must exist") - )) - )); - } - - Ok(migration_output) -} - -async fn load_world_manifests( - profile_dir: &Utf8PathBuf, - account: &SingleOwnerAccount, - world_address: Option, - ui: &Ui, -) -> Result<(BaseManifest, Option)> -where - P: Provider + Sync + Send + 'static, - S: Signer + Sync + Send + 'static, -{ - ui.print_step(1, "šŸŒŽ", "Building World state..."); - - let mut local_manifest = BaseManifest::load_from_path(&profile_dir.join(BASE_DIR)) - .map_err(|e| anyhow!("Fail to load local manifest file: {e}."))?; - - let overlay_path = profile_dir.join(OVERLAYS_DIR); - if overlay_path.exists() { - let overlay_manifest = OverlayManifest::load_from_path(&profile_dir.join(OVERLAYS_DIR)) - .map_err(|e| anyhow!("Fail to load overlay manifest file: {e}."))?; - - // merge user defined changes to base manifest - local_manifest.merge(overlay_manifest); - } - - let remote_manifest = if let Some(address) = world_address { - match DeploymentManifest::load_from_remote(account.provider(), address).await { - Ok(manifest) => { - ui.print_sub(format!("Found remote World: {address:#x}")); - Some(manifest) - } - Err(AbstractManifestError::RemoteWorldNotFound) => None, - Err(e) => { - ui.verbose(format!("{e:?}")); - return Err(anyhow!("Failed to build remote World state: {e}")); - } - } - } else { - None - }; - - if remote_manifest.is_none() { - ui.print_sub("No remote World found"); - } - - Ok((local_manifest, remote_manifest)) -} - -pub fn prepare_migration( - target_dir: &Utf8PathBuf, - diff: WorldDiff, - name: Option, - world_address: Option, - ui: &Ui, -) -> Result { - ui.print_step(3, "šŸ“¦", "Preparing for migration..."); - - if name.is_none() && !diff.world.is_same() { - bail!( - "World name is required when attempting to migrate the World contract. Please provide \ - it using `--name`." - ); - } - - let name = if let Some(name) = name { - Some(cairo_short_string_to_felt(&name).with_context(|| "Failed to parse World name.")?) - } else { - None - }; - - let migration = prepare_for_migration(world_address, name, target_dir, diff) - .with_context(|| "Problem preparing for migration.")?; - - let info = migration.info(); - - ui.print_sub(format!( - "Total items to be migrated ({}): New {} Update {}", - info.new + info.update, - info.new, - info.update - )); - - Ok(migration) -} - -pub async fn execute_strategy( - ws: &Workspace<'_>, - strategy: &mut MigrationStrategy, - migrator: &SingleOwnerAccount, - txn_config: TxnConfig, -) -> Result -where - P: Provider + Sync + Send + 'static, - S: Signer + Sync + Send + 'static, -{ - let ui = ws.config().ui(); - let mut world_tx_hash: Option = None; - let mut world_block_number: Option = None; - - match &strategy.base { - Some(base) => { - ui.print_header("# Base Contract"); - - match base.declare(migrator, &txn_config).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(MigrationError::ArtifactError(e)) => { - return Err(handle_artifact_error(&ui, base.artifact_path(), e)); - } - Err(e) => { - ui.verbose(format!("{e:?}")); - return Err(e.into()); - } - }; - } - None => {} - }; - - match &strategy.world { - Some(world) => { - ui.print_header("# World"); - - // 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}") - })?; - - ui.print_sub(format!( - "Upgraded Contract at address: {:#x}", - world.contract_address - )); - } else { - 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) - }; + update_manifests_and_abis( + ws, + local_manifest.clone(), + &profile_dir, + &profile_name, + &rpc_url, + world_address, + migration_output.clone(), + name, + ) + .await?; - ui.print_sub(format!("Contract address: {:#x}", world.contract_address)); + if let Some(migration_output) = migration_output { + if !ws.config().offline() { + upload_metadata(ws, account, migration_output, txn_config).await?; } } - None => {} - }; - - let mut migration_output = MigrationOutput { - world_address: strategy.world_address()?, - world_tx_hash, - world_block_number, - full: false, - models: vec![], - contracts: vec![], - }; - - // Once Torii supports indexing arrays, we should declare and register the - // ResourceMetadata model. - match register_models(strategy, migrator, &ui, &txn_config).await { - Ok(output) => { - migration_output.models = output.registered_model_names; - } - Err(e) => { - ui.anyhow(&e); - return Ok(migration_output); - } }; - match deploy_dojo_contracts(strategy, migrator, &ui, &txn_config).await { - Ok(output) => { - migration_output.contracts = output; - } - Err(e) => { - ui.anyhow(&e); - return Ok(migration_output); - } - }; - - migration_output.full = true; - - Ok(migration_output) + Ok(()) } enum ContractDeploymentOutput { @@ -571,489 +155,3 @@ enum ContractDeploymentOutput { enum ContractUpgradeOutput { Output(UpgradeOutput), } - -async fn deploy_contract( - contract: &ContractMigration, - contract_id: &str, - constructor_calldata: Vec, - migrator: &SingleOwnerAccount, - ui: &Ui, - txn_config: &TxnConfig, -) -> Result -where - P: Provider + Sync + Send + 'static, - S: Signer + Sync + Send + 'static, -{ - match contract - .deploy(contract.diff.local_class_hash, constructor_calldata, migrator, txn_config) - .await - { - Ok(mut val) => { - if let Some(declare) = val.clone().declare { - ui.print_hidden_sub(format!( - "Declare transaction: {:#x}", - declare.transaction_hash - )); - } - - ui.print_hidden_sub(format!("Deploy transaction: {:#x}", val.transaction_hash)); - - val.name = Some(contract.diff.name.clone()); - Ok(ContractDeploymentOutput::Output(val)) - } - Err(MigrationError::ContractAlreadyDeployed(contract_address)) => { - Ok(ContractDeploymentOutput::AlreadyDeployed(contract_address)) - } - Err(MigrationError::ArtifactError(e)) => { - return Err(handle_artifact_error(ui, contract.artifact_path(), e)); - } - Err(e) => { - ui.verbose(format!("{e:?}")); - Err(anyhow!("Failed to migrate {contract_id}: {e}")) - } - } -} - -async fn upgrade_contract( - contract: &ContractMigration, - contract_id: &str, - original_class_hash: FieldElement, - original_base_class_hash: FieldElement, - migrator: &SingleOwnerAccount, - ui: &Ui, - txn_config: &TxnConfig, -) -> 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, - ) - .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, - ui: &Ui, - txn_config: &TxnConfig, -) -> Result -where - P: Provider + Sync + Send + 'static, - S: Signer + Sync + Send + 'static, -{ - let models = &strategy.models; - - if models.is_empty() { - return Ok(RegisterOutput { - transaction_hash: FieldElement::ZERO, - declare_output: vec![], - registered_model_names: vec![], - }); - } - - ui.print_header(format!("# Models ({})", models.len())); - - let mut declare_output = vec![]; - let mut registered_model_names = vec![]; - - for c in models.iter() { - ui.print(italic_message(&c.diff.name).to_string()); - - let res = c.declare(migrator, txn_config).await; - match res { - Ok(output) => { - ui.print_hidden_sub(format!("Declare transaction: {:#x}", output.transaction_hash)); - - declare_output.push(output); - } - - // Continue if model is already declared - Err(MigrationError::ClassAlreadyDeclared) => { - ui.print_sub(format!("Already declared: {:#x}", c.diff.local)); - continue; - } - Err(MigrationError::ArtifactError(e)) => { - return Err(handle_artifact_error(ui, c.artifact_path(), e)); - } - Err(e) => { - ui.verbose(format!("{e:?}")); - bail!("Failed to declare model {}: {e}", c.diff.name) - } - } - - ui.print_sub(format!("Class hash: {:#x}", c.diff.local)); - } - - let world_address = strategy.world_address()?; - let world = WorldContract::new(world_address, migrator); - - let calls = models - .iter() - .map(|c| { - registered_model_names.push(c.diff.name.clone()); - world.register_model_getcall(&c.diff.local.into()) - }) - .collect::>(); - - let InvokeTransactionResult { transaction_hash } = - world.account.execute(calls).send_with_cfg(txn_config).await.map_err(|e| { - ui.verbose(format!("{e:?}")); - anyhow!("Failed to register models to World: {e}") - })?; - - TransactionWaiter::new(transaction_hash, migrator.provider()).await?; - - ui.print(format!("All models are registered at: {transaction_hash:#x}")); - - Ok(RegisterOutput { transaction_hash, declare_output, registered_model_names }) -} - -async fn deploy_dojo_contracts( - strategy: &mut MigrationStrategy, - migrator: &SingleOwnerAccount, - ui: &Ui, - txn_config: &TxnConfig, -) -> Result>> -where - P: Provider + Sync + Send + 'static, - S: Signer + Sync + Send + 'static, -{ - let contracts = &strategy.contracts; - - if contracts.is_empty() { - return Ok(vec![]); - } - - ui.print_header(format!("# Contracts ({})", contracts.len())); - - let mut deploy_output = vec![]; - - let world_address = strategy.world_address()?; - - let contracts = &mut strategy.contracts; - for contract in contracts { - let name = &contract.diff.name; - ui.print(italic_message(name).to_string()); - match contract - .deploy_dojo_contract( - world_address, - contract.diff.local_class_hash, - contract.diff.base_class_hash, - migrator, - txn_config, - ) - .await - { - Ok(output) => { - if let Some(ref declare) = output.declare { - ui.print_hidden_sub(format!( - "Declare transaction: {:#x}", - declare.transaction_hash - )); - } - - contract.contract_address = 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(ContractMigrationOutput { - name: name.to_string(), - contract_address: output.contract_address, - base_class_hash: output.base_class_hash, - })); - } - Err(MigrationError::ContractAlreadyDeployed(contract_address)) => { - ui.print_sub(format!("Already deployed: {:#x}", contract_address)); - deploy_output.push(None); - } - Err(MigrationError::ArtifactError(e)) => { - return Err(handle_artifact_error(ui, contract.artifact_path(), e)); - } - Err(e) => { - ui.verbose(format!("{e:?}")); - return Err(anyhow!("Failed to migrate {name}: {e}")); - } - } - } - - Ok(deploy_output) -} - -pub fn handle_artifact_error(ui: &Ui, artifact_path: &Path, error: anyhow::Error) -> anyhow::Error { - let path = artifact_path.to_string_lossy(); - let name = artifact_path.file_name().unwrap().to_string_lossy(); - ui.verbose(format!("{path}: {error:?}")); - - anyhow!( - "Discrepancy detected in {name}.\nUse `sozo clean` to clean your project or `sozo clean \ - --artifacts` to clean artifacts only.\nThen, rebuild your project with `sozo build`." - ) -} - -pub async fn get_contract_operation_name

( - provider: &P, - contract: &ContractMigration, - world_address: Option, -) -> String -where - P: Provider + Sync + Send + 'static, -{ - 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); - - 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.name); - } - Err(ProviderError::StarknetError(StarknetError::ContractNotFound)) => { - return format!("deploy {}", contract.diff.name); - } - Ok(_) => return "already deployed".to_string(), - Err(_) => return format!("deploy {}", contract.diff.name), - } - } - } - format!("deploy {}", contract.diff.name) -} - -pub async fn print_strategy

(ui: &Ui, provider: &P, strategy: &MigrationStrategy) -where - P: Provider + Sync + Send + 'static, -{ - ui.print("\nšŸ“‹ Migration Strategy\n"); - - if let Some(base) = &strategy.base { - ui.print_header("# Base Contract"); - ui.print_sub(format!("declare (class hash: {:#x})\n", base.diff.local)); - } - - if let Some(world) = &strategy.world { - ui.print_header("# World"); - ui.print_sub(format!("declare (class hash: {:#x})\n", world.diff.local_class_hash)); - } - - if !&strategy.models.is_empty() { - ui.print_header(format!("# Models ({})", &strategy.models.len())); - for m in &strategy.models { - ui.print_sub(format!("register {} (class hash: {:#x})", m.diff.name, m.diff.local)); - } - ui.print(" "); - } - - if !&strategy.contracts.is_empty() { - 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_class_hash)); - } - ui.print(" "); - } -} - -/// Upload a metadata as a IPFS artifact and then create a resource to register -/// 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. -/// -/// # Returns -/// A [`ResourceData`] object to register in the Dojo resource register -/// on success. -async fn upload_on_ipfs_and_create_resource( - ui: &Ui, - element_name: String, - resource_id: FieldElement, - artifact: ArtifactMetadata, -) -> Result { - match artifact.upload().await { - Ok(hash) => { - ui.print_sub(format!("{}: ipfs://{}", element_name, hash)); - create_resource_metadata(resource_id, hash) - } - Err(_) => Err(anyhow!("Failed to upload IPFS resource.")), - } -} - -/// Create a resource to register in the Dojo resource registry. -/// -/// # Arguments -/// * `resource_id` - the ID of the resource -/// * `hash` - the IPFS hash -/// -/// # Returns -/// A [`ResourceData`] object to register in the Dojo resource register -/// on success. -fn create_resource_metadata(resource_id: FieldElement, hash: String) -> Result { - 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()]); - } - - Ok(ResourceMetadata { resource_id, metadata_uri: encoded_uri }) -} - -/// Upload metadata of the world/models/contracts as IPFS artifacts and then -/// register them in the Dojo resource registry. -/// -/// # Arguments -/// -/// * `ws` - the workspace -/// * `migrator` - the account used to migrate -/// * `migration_output` - the output after having applied the migration plan. -pub async fn upload_metadata( - ws: &Workspace<'_>, - migrator: &SingleOwnerAccount, - migration_output: MigrationOutput, - txn_config: TxnConfig, -) -> Result<()> -where - P: Provider + Sync + Send + 'static, - S: Signer + Sync + Send + 'static, -{ - let ui = ws.config().ui(); - - ui.print(" "); - ui.print_step(6, "šŸŒ", "Uploading metadata..."); - ui.print(" "); - - let dojo_metadata = dojo_metadata_from_workspace(ws); - let mut ipfs = vec![]; - let mut resources = vec![]; - - // world - if migration_output.world_tx_hash.is_some() { - match dojo_metadata.world.upload().await { - Ok(hash) => { - let resource = create_resource_metadata(FieldElement::ZERO, hash.clone())?; - ui.print_sub(format!("world: ipfs://{}", hash)); - resources.push(resource); - } - Err(err) => { - ui.print_sub(format!("Failed to upload World metadata:\n{err}")); - } - } - } - - // models - if !migration_output.models.is_empty() { - for model_name in migration_output.models { - if let Some(m) = dojo_metadata.artifacts.get(&model_name) { - ipfs.push(upload_on_ipfs_and_create_resource( - &ui, - model_name.clone(), - get_selector_from_name(&model_name).expect("ASCII model name"), - m.clone(), - )); - } - } - } - - // contracts - let migrated_contracts = migration_output.contracts.into_iter().flatten().collect::>(); - - if !migrated_contracts.is_empty() { - for contract in migrated_contracts { - if let Some(m) = dojo_metadata.artifacts.get(&contract.name) { - ipfs.push(upload_on_ipfs_and_create_resource( - &ui, - contract.name.clone(), - contract.contract_address, - m.clone(), - )); - } - } - } - - // upload IPFS - resources.extend( - future::try_join_all(ipfs) - .await - .map_err(|_| anyhow!("Unable to upload IPFS artifacts."))?, - ); - - ui.print("> All IPFS artifacts have been successfully uploaded.".to_string()); - - // update the resource registry - let world = WorldContract::new(migration_output.world_address, migrator); - - let calls = resources.iter().map(|r| world.set_metadata_getcall(r)).collect::>(); - - let InvokeTransactionResult { transaction_hash } = - migrator.execute(calls).send_with_cfg(&txn_config).await.map_err(|e| { - ui.verbose(format!("{e:?}")); - anyhow!("Failed to register metadata into the resource registry: {e}") - })?; - - TransactionWaiter::new(transaction_hash, migrator.provider()).await?; - - ui.print(format!( - "> All metadata have been registered in the resource registry (tx hash: \ - {transaction_hash:#x})" - )); - - ui.print(""); - ui.print("\nāœØ Done."); - - Ok(()) -} diff --git a/crates/sozo/ops/src/migration/utils.rs b/crates/sozo/ops/src/migration/utils.rs new file mode 100644 index 0000000000..d21e7c4c29 --- /dev/null +++ b/crates/sozo/ops/src/migration/utils.rs @@ -0,0 +1,63 @@ +use anyhow::{anyhow, Result}; +use camino::Utf8PathBuf; +use dojo_lang::compiler::{BASE_DIR, OVERLAYS_DIR}; +use dojo_world::manifest::{ + AbstractManifestError, BaseManifest, DeploymentManifest, OverlayManifest, +}; +use scarb_ui::Ui; +use starknet::accounts::{ConnectedAccount, SingleOwnerAccount}; +use starknet::providers::Provider; +use starknet::signers::Signer; +use starknet_crypto::FieldElement; + +use super::ui::MigrationUi; + +/// Loads: +/// - `BaseManifest` from filesystem +/// - `DeployedManifest` from onchain dataa if `world_address` is `Some` +pub(super) async fn load_world_manifests( + profile_dir: &Utf8PathBuf, + account: &SingleOwnerAccount, + world_address: Option, + ui: &Ui, +) -> Result<(BaseManifest, Option)> +where + P: Provider + Sync + Send, + S: Signer + Sync + Send, +{ + ui.print_step(1, "šŸŒŽ", "Building World state..."); + + let mut local_manifest = BaseManifest::load_from_path(&profile_dir.join(BASE_DIR)) + .map_err(|e| anyhow!("Fail to load local manifest file: {e}."))?; + + let overlay_path = profile_dir.join(OVERLAYS_DIR); + if overlay_path.exists() { + let overlay_manifest = OverlayManifest::load_from_path(&profile_dir.join(OVERLAYS_DIR)) + .map_err(|e| anyhow!("Fail to load overlay manifest file: {e}."))?; + + // merge user defined changes to base manifest + local_manifest.merge(overlay_manifest); + } + + let remote_manifest = if let Some(address) = world_address { + match DeploymentManifest::load_from_remote(account.provider(), address).await { + Ok(manifest) => { + ui.print_sub(format!("Found remote World: {address:#x}")); + Some(manifest) + } + Err(AbstractManifestError::RemoteWorldNotFound) => None, + Err(e) => { + ui.verbose(format!("{e:?}")); + return Err(anyhow!("Failed to build remote World state: {e}")); + } + } + } else { + None + }; + + if remote_manifest.is_none() { + ui.print_sub("No remote World found"); + } + + Ok((local_manifest, remote_manifest)) +} diff --git a/crates/sozo/ops/src/tests/migration.rs b/crates/sozo/ops/src/tests/migration.rs index 356707db89..54f25c79e7 100644 --- a/crates/sozo/ops/src/tests/migration.rs +++ b/crates/sozo/ops/src/tests/migration.rs @@ -3,6 +3,7 @@ use std::str; use camino::Utf8Path; use dojo_lang::compiler::{BASE_DIR, MANIFESTS_DIR}; use dojo_test_utils::compiler::build_full_test_config; +use dojo_test_utils::migration::prepare_migration_with_world_and_seed; use dojo_test_utils::sequencer::{ get_default_test_starknet_config, SequencerConfig, StarknetConfig, TestSequencer, }; @@ -36,7 +37,7 @@ async fn migrate_with_auto_mine() { let config = load_config(); let ws = setup_ws(&config); - let mut migration = setup_migration().unwrap(); + let migration = setup_migration().unwrap(); let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; @@ -44,7 +45,7 @@ async fn migrate_with_auto_mine() { let mut account = sequencer.account(); account.set_block_id(BlockId::Tag(BlockTag::Pending)); - execute_strategy(&ws, &mut migration, &account, TxnConfig::default()).await.unwrap(); + execute_strategy(&ws, &migration, &account, TxnConfig::default()).await.unwrap(); sequencer.stop().unwrap(); } @@ -54,7 +55,7 @@ async fn migrate_with_block_time() { let config = load_config(); let ws = setup_ws(&config); - let mut migration = setup_migration().unwrap(); + let migration = setup_migration().unwrap(); let sequencer = TestSequencer::start( SequencerConfig { block_time: Some(1000), ..Default::default() }, @@ -65,7 +66,7 @@ async fn migrate_with_block_time() { let mut account = sequencer.account(); account.set_block_id(BlockId::Tag(BlockTag::Pending)); - execute_strategy(&ws, &mut migration, &account, TxnConfig::default()).await.unwrap(); + execute_strategy(&ws, &migration, &account, TxnConfig::default()).await.unwrap(); sequencer.stop().unwrap(); } @@ -74,7 +75,7 @@ async fn migrate_with_small_fee_multiplier_will_fail() { let config = load_config(); let ws = setup_ws(&config); - let mut migration = setup_migration().unwrap(); + let migration = setup_migration().unwrap(); let sequencer = TestSequencer::start( Default::default(), @@ -95,7 +96,7 @@ async fn migrate_with_small_fee_multiplier_will_fail() { assert!( execute_strategy( &ws, - &mut migration, + &migration, &account, TxnConfig { fee_estimate_multiplier: Some(0.2f64), ..Default::default() }, ) @@ -105,20 +106,6 @@ async fn migrate_with_small_fee_multiplier_will_fail() { sequencer.stop().unwrap(); } -#[test] -fn migrate_world_without_seed_will_fail() { - let profile_name = "dev"; - let base = "../../../examples/spawn-and-move"; - let target_dir = format!("{}/target/dev", base); - let manifest = BaseManifest::load_from_path( - &Utf8Path::new(base).to_path_buf().join(MANIFESTS_DIR).join(profile_name).join(BASE_DIR), - ) - .unwrap(); - let world = WorldDiff::compute(manifest, None); - let res = prepare_for_migration(None, None, &Utf8Path::new(&target_dir).to_path_buf(), world); - assert!(res.is_err_and(|e| e.to_string().contains("Missing seed for World deployment."))) -} - #[tokio::test] async fn migration_from_remote() { let config = load_config(); @@ -149,15 +136,15 @@ async fn migration_from_remote() { let world = WorldDiff::compute(manifest, None); - let mut migration = prepare_for_migration( + let migration = prepare_for_migration( None, - Some(felt!("0x12345")), + felt!("0x12345"), &Utf8Path::new(&target_dir).to_path_buf(), world, ) .unwrap(); - execute_strategy(&ws, &mut migration, &account, TxnConfig::default()).await.unwrap(); + execute_strategy(&ws, &migration, &account, TxnConfig::default()).await.unwrap(); let local_manifest = BaseManifest::load_from_path( &Utf8Path::new(base).to_path_buf().join(MANIFESTS_DIR).join(&profile_name).join(BASE_DIR), @@ -183,7 +170,7 @@ async fn migrate_with_metadata() { .unwrap_or_else(|c| panic!("Error loading config: {c:?}")); let ws = setup_ws(&config); - let mut migration = setup_migration().unwrap(); + let migration = setup_migration().unwrap(); let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; @@ -191,8 +178,7 @@ async fn migrate_with_metadata() { let mut account = sequencer.account(); account.set_block_id(BlockId::Tag(BlockTag::Pending)); - let output = - execute_strategy(&ws, &mut migration, &account, TxnConfig::default()).await.unwrap(); + let output = execute_strategy(&ws, &migration, &account, TxnConfig::default()).await.unwrap(); let res = upload_metadata(&ws, &account, output.clone(), TxnConfig::default()).await; assert!(res.is_ok()); @@ -252,6 +238,25 @@ async fn migrate_with_metadata() { } } +#[tokio::test(flavor = "multi_thread")] +async fn migration_with_mismatching_world_address_and_seed() { + let base_dir = "../../../examples/spawn-and-move"; + let target_dir = format!("{}/target/dev", base_dir); + + let result = prepare_migration_with_world_and_seed( + base_dir.into(), + target_dir.into(), + Some(felt!("0x1")), + "sozo_test", + ); + + assert!(result.is_err_and(|e| e.to_string().contains( + "Calculated world address doesn't match provided world address.\nIf you are deploying \ + with custom seed make sure `world_address` is correctly configured (or not set) \ + `Scarb.toml`" + ))); +} + /// Get the hash from a IPFS URI /// /// # Arguments diff --git a/crates/sozo/ops/src/tests/setup.rs b/crates/sozo/ops/src/tests/setup.rs index 14bc1624fa..7c0777d937 100644 --- a/crates/sozo/ops/src/tests/setup.rs +++ b/crates/sozo/ops/src/tests/setup.rs @@ -1,6 +1,6 @@ use anyhow::Result; use dojo_test_utils::compiler::build_test_config; -use dojo_test_utils::migration::prepare_migration; +use dojo_test_utils::migration::prepare_migration_with_world_and_seed; use dojo_test_utils::sequencer::TestSequencer; use dojo_world::contracts::world::WorldContract; use dojo_world::migration::strategy::MigrationStrategy; @@ -47,7 +47,7 @@ pub fn setup_migration() -> Result { let base_dir = "../../../examples/spawn-and-move"; let target_dir = format!("{}/target/dev", base_dir); - prepare_migration(base_dir.into(), target_dir.into()) + prepare_migration_with_world_and_seed(base_dir.into(), target_dir.into(), None, "sozo_test") } /// Setups the project by migrating the full spawn-and-moves project. @@ -66,14 +66,14 @@ pub async fn setup( let config = load_config(); let ws = setup_ws(&config); - let mut migration = setup_migration()?; + let migration = setup_migration()?; let mut account = sequencer.account(); account.set_block_id(BlockId::Tag(BlockTag::Pending)); let output = migration::execute_strategy( &ws, - &mut migration, + &migration, &account, TxnConfig { wait: true, ..Default::default() }, ) diff --git a/crates/torii/core/src/sql_test.rs b/crates/torii/core/src/sql_test.rs index 8dafa934fd..b441c619f6 100644 --- a/crates/torii/core/src/sql_test.rs +++ b/crates/torii/core/src/sql_test.rs @@ -59,7 +59,7 @@ async fn test_load_from_remote() { sqlx::migrate!("../migrations").run(&pool).await.unwrap(); let base_path = "../../../examples/spawn-and-move"; let target_path = format!("{}/target/dev", base_path); - let mut migration = prepare_migration(base_path.into(), target_path.into()).unwrap(); + let migration = prepare_migration(base_path.into(), target_path.into()).unwrap(); let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; let provider = JsonRpcClient::new(HttpTransport::new(sequencer.url())); @@ -71,12 +71,22 @@ async fn test_load_from_remote() { let config = build_test_config("../../../examples/spawn-and-move/Scarb.toml").unwrap(); let ws = ops::read_workspace(config.manifest_path(), &config) .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); - execute_strategy(&ws, &mut migration, &account, TxnConfig::default()).await.unwrap(); + let migration_output = + execute_strategy(&ws, &migration, &account, TxnConfig::default()).await.unwrap(); + let world_address = migration_output.world_address; + + assert!(migration.world_address().unwrap() == world_address); // spawn let tx = account .execute(vec![Call { - to: migration.contracts.first().unwrap().contract_address, + to: migration_output + .contracts + .first() + .expect("shouldn't be empty") + .as_ref() + .expect("should be deployed") + .contract_address, selector: get_selector_from_name("spawn").unwrap(), calldata: vec![], }]) @@ -86,7 +96,7 @@ async fn test_load_from_remote() { TransactionWaiter::new(tx.transaction_hash, &provider).await.unwrap(); - let mut db = Sql::new(pool.clone(), migration.world_address().unwrap()).await.unwrap(); + let mut db = Sql::new(pool.clone(), world_address).await.unwrap(); let _ = bootstrap_engine(world, db.clone(), &provider).await; let _block_timestamp = 1710754478_u64; diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index 15b6a89d8d..f5b7aa7511 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -278,7 +278,7 @@ pub async fn spinup_types_test() -> Result { let base_path = "../types-test"; let target_path = format!("{}/target/dev", base_path); - let mut migration = prepare_migration(base_path.into(), target_path.into()).unwrap(); + let migration = prepare_migration(base_path.into(), target_path.into()).unwrap(); let config = build_test_config("../types-test/Scarb.toml").unwrap(); let db = Sql::new(pool.clone(), migration.world_address().unwrap()).await.unwrap(); @@ -293,7 +293,7 @@ pub async fn spinup_types_test() -> Result { let ws = ops::read_workspace(config.manifest_path(), &config) .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); - execute_strategy(&ws, &mut migration, &account, TxnConfig::default()).await.unwrap(); + execute_strategy(&ws, &migration, &account, TxnConfig::default()).await.unwrap(); let manifest = DeploymentManifest::load_from_remote(&provider, migration.world_address().unwrap()) diff --git a/crates/torii/grpc/src/server/tests/entities_test.rs b/crates/torii/grpc/src/server/tests/entities_test.rs index 660438d83e..e4dcec6977 100644 --- a/crates/torii/grpc/src/server/tests/entities_test.rs +++ b/crates/torii/grpc/src/server/tests/entities_test.rs @@ -36,7 +36,7 @@ async fn test_entities_queries() { sqlx::migrate!("../migrations").run(&pool).await.unwrap(); let base_path = "../../../examples/spawn-and-move"; let target_path = format!("{}/target/dev", base_path); - let mut migration = prepare_migration(base_path.into(), target_path.into()).unwrap(); + let migration = prepare_migration(base_path.into(), target_path.into()).unwrap(); let sequencer = TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(sequencer.url()))); @@ -48,12 +48,21 @@ async fn test_entities_queries() { let config = build_test_config("../../../examples/spawn-and-move/Scarb.toml").unwrap(); let ws = ops::read_workspace(config.manifest_path(), &config) .unwrap_or_else(|op| panic!("Error building workspace: {op:?}")); - execute_strategy(&ws, &mut migration, &account, TxnConfig::default()).await.unwrap(); + let migration_output = + execute_strategy(&ws, &migration, &account, TxnConfig::default()).await.unwrap(); + + let world_address = migration_output.world_address; // spawn let tx = account .execute(vec![Call { - to: migration.contracts.first().unwrap().contract_address, + to: migration_output + .contracts + .first() + .expect("shouldn't be empty") + .as_ref() + .expect("should be deployed") + .contract_address, selector: get_selector_from_name("spawn").unwrap(), calldata: vec![], }]) @@ -63,7 +72,7 @@ async fn test_entities_queries() { TransactionWaiter::new(tx.transaction_hash, &provider).await.unwrap(); - let db = Sql::new(pool.clone(), migration.world_address().unwrap()).await.unwrap(); + let db = Sql::new(pool.clone(), world_address).await.unwrap(); let (shutdown_tx, _) = broadcast::channel(1); let mut engine = Engine::new( @@ -82,8 +91,7 @@ async fn test_entities_queries() { let _ = engine.sync_to_head(0).await.unwrap(); let (_, receiver) = tokio::sync::mpsc::channel(1); - let grpc = - DojoWorld::new(db.pool, receiver, migration.world_address().unwrap(), provider.clone()); + let grpc = DojoWorld::new(db.pool, receiver, world_address, provider.clone()); let entities = grpc .query_by_keys(