From 1b75ac9deea8a17fe1b686b77651f1b9647132cb Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Fri, 2 Aug 2024 04:23:52 +0800 Subject: [PATCH] refactor(sozo): deploy controller account if not exist (#2242) --- Cargo.lock | 1 + bin/sozo/Cargo.toml | 3 +- .../commands/options/account/controller.rs | 133 ++++++++++++++++-- bin/sozo/tests/test_data/policies.json | 126 +++++++++++++++++ 4 files changed, 250 insertions(+), 13 deletions(-) create mode 100644 bin/sozo/tests/test_data/policies.json diff --git a/Cargo.lock b/Cargo.lock index 70c57f440a..5340c8fcd7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -12730,6 +12730,7 @@ dependencies = [ "num-bigint", "num-integer", "prettytable-rs", + "reqwest 0.12.5", "rpassword", "scarb", "scarb-ui", diff --git a/bin/sozo/Cargo.toml b/bin/sozo/Cargo.toml index 6d7b3c651a..c7a5b018e4 100644 --- a/bin/sozo/Cargo.toml +++ b/bin/sozo/Cargo.toml @@ -59,6 +59,7 @@ tracing.workspace = true url.workspace = true cainome.workspace = true +reqwest = { workspace = true, features = [ "json" ], optional = true } [dev-dependencies] assert_fs.workspace = true @@ -67,5 +68,5 @@ katana-runner.workspace = true snapbox = "0.4.6" [features] -controller = [ "dep:account_sdk", "dep:slot" ] +controller = [ "dep:account_sdk", "dep:slot", "dep:reqwest" ] default = [ "controller" ] diff --git a/bin/sozo/src/commands/options/account/controller.rs b/bin/sozo/src/commands/options/account/controller.rs index d0390349c9..8b5e154390 100644 --- a/bin/sozo/src/commands/options/account/controller.rs +++ b/bin/sozo/src/commands/options/account/controller.rs @@ -1,21 +1,23 @@ use account_sdk::account::session::hash::{AllowedMethod, Session}; use account_sdk::account::session::SessionAccount; use account_sdk::signers::HashSigner; -use anyhow::{Context, Result}; +use anyhow::{bail, Context, Result}; use camino::{Utf8Path, Utf8PathBuf}; use dojo_world::contracts::naming::get_name_from_tag; -use dojo_world::manifest::{BaseManifest, DojoContract, Manifest}; +use dojo_world::manifest::{BaseManifest, Class, DojoContract, Manifest}; use dojo_world::migration::strategy::generate_salt; use scarb::core::Config; use slot::session::Policy; use starknet::core::types::contract::{AbiEntry, StateMutability}; -use starknet::core::types::Felt; +use starknet::core::types::StarknetError::ContractNotFound; +use starknet::core::types::{BlockId, BlockTag, Felt}; use starknet::core::utils::{cairo_short_string_to_felt, get_contract_address}; use starknet::macros::{felt, short_string}; use starknet::providers::Provider; +use starknet::providers::ProviderError::StarknetError; use starknet::signers::SigningKey; use starknet_crypto::poseidon_hash_single; -use tracing::trace; +use tracing::{trace, warn}; use url::Url; use super::WorldAddressOrName; @@ -61,7 +63,9 @@ where // Perform policies diff check. For security reasons, we will always create a new // session here if the current policies are different from the existing - // session. TODO(kariy): maybe don't need to update if current policies is a + // session. + // + // TODO(kariy): maybe don't need to update if current policies is a // subset of the existing policies. let policies = collect_policies(world_addr_or_name, contract_address, config)?; @@ -72,7 +76,7 @@ where "Policies have changed. Creating new session." ); - let session = slot::session::create(rpc_url, &policies).await?; + let session = slot::session::create(rpc_url.clone(), &policies).await?; slot::session::store(chain_id, &session)?; session } else { @@ -84,7 +88,7 @@ where None => { trace!(%username, chain = format!("{chain_id:#}"), "Creating new session."); let policies = collect_policies(world_addr_or_name, contract_address, config)?; - let session = slot::session::create(rpc_url, &policies).await?; + let session = slot::session::create(rpc_url.clone(), &policies).await?; slot::session::store(chain_id, &session)?; session } @@ -103,6 +107,11 @@ where let expires_at = session_details.expires_at.parse::()?; let session = Session::new(methods, expires_at, &signer.signer())?; + // make sure account exist on the provided chain, if not, we deploy it first before proceeding + deploy_account_if_not_exist(rpc_url, &provider, chain_id, contract_address, &username) + .await + .with_context(|| format!("Deploying Controller account on chain {chain_id}"))?; + let session_account = SessionAccount::new( provider, signer, @@ -154,7 +163,7 @@ fn collect_policies_from_base_manifest( // get methods from all project contracts for contract in manifest.contracts { - let contract_address = get_dojo_contract_address(world_address, &contract); + let contract_address = get_dojo_contract_address(world_address, &contract, &manifest.base); let abis = contract.inner.abi.unwrap().load_abi_string(&base_path)?; let abis = serde_json::from_str::>(&abis)?; policies_from_abis(&mut policies, &contract.inner.tag, contract_address, &abis); @@ -209,12 +218,24 @@ fn policies_from_abis( } } -fn get_dojo_contract_address(world_address: Felt, manifest: &Manifest) -> Felt { - if let Some(address) = manifest.inner.address { +fn get_dojo_contract_address( + world_address: Felt, + contract: &Manifest, + base_class: &Manifest, +) -> Felt { + // The `base_class_hash` field in the Contract's base manifest is initially set to ZERO, + // so we need to use the `class_hash` from the base class manifest instead. + let base_class_hash = if contract.inner.base_class_hash != Felt::ZERO { + contract.inner.base_class_hash + } else { + base_class.inner.class_hash + }; + + if let Some(address) = contract.inner.address { address } else { - let salt = generate_salt(&get_name_from_tag(&manifest.inner.tag)); - get_contract_address(salt, manifest.inner.base_class_hash, &[], world_address) + let salt = generate_salt(&get_name_from_tag(&contract.inner.tag)); + get_contract_address(salt, base_class_hash, &[], world_address) } } @@ -237,3 +258,91 @@ fn get_dojo_world_address( } } } + +/// This function will call the `cartridge_deployController` method to deploy the account if it +/// doesn't yet exist on the chain. But this JSON-RPC method is only available on Katana deployed on +/// Slot. If the `rpc_url` is not a Slot url, it will return an error. +/// +/// `cartridge_deployController` is not a method that Katana itself exposes. It's from a middleware +/// layer that is deployed on top of the Katana deployment on Slot. This method will deploy the +/// contract of a user based on the Slot deployment. +async fn deploy_account_if_not_exist( + rpc_url: Url, + provider: &impl Provider, + chain_id: Felt, + address: Felt, + username: &str, +) -> Result<()> { + use reqwest::Client; + use serde_json::json; + + // Check if the account exists on the chain + match provider.get_class_at(BlockId::Tag(BlockTag::Pending), address).await { + Ok(_) => Ok(()), + + // if account doesn't exist, deploy it by calling `cartridge_deployController` method + Err(err @ StarknetError(ContractNotFound)) => { + trace!( + %username, + chain = format!("{chain_id:#}"), + address = format!("{address:#x}"), + "Controller does not exist on chain. Attempting to deploy..." + ); + + // Skip deployment if the rpc_url is not a Slot instance + if !rpc_url.host_str().map_or(false, |host| host.contains("api.cartridge.gg")) { + warn!(%rpc_url, "Unable to deploy Controller on non-Slot instance."); + bail!("Controller with username '{username}' does not exist: {err}"); + } + + let body = json!({ + "id": 1, + "jsonrpc": "2.0", + "params": { "id": username }, + "method": "cartridge_deployController", + }); + + let _ = Client::new() + .post(rpc_url) + .json(&body) + .send() + .await? + .error_for_status() + .with_context(|| "Failed to deploy controller")?; + + Ok(()) + } + + Err(e) => bail!(e), + } +} + +#[cfg(test)] +mod tests { + use dojo_test_utils::compiler::CompilerTestSetup; + use scarb::compiler::Profile; + use starknet::macros::felt; + + use super::{collect_policies, Policy}; + use crate::commands::options::account::WorldAddressOrName; + + #[test] + fn collect_policies_from_project() { + let config = CompilerTestSetup::from_examples("../../crates/dojo-core", "../../examples/") + .build_test_config("spawn-and-move", Profile::DEV); + + let world_addr = felt!("0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a"); + let user_addr = felt!("0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03"); + + let policies = + collect_policies(WorldAddressOrName::Address(world_addr), user_addr, &config).unwrap(); + + // Get test data + let test_data = include_str!("../../../../tests/test_data/policies.json"); + let expected_policies: Vec = serde_json::from_str(test_data).unwrap(); + + // Compare the collected policies with the test data + assert_eq!(policies.len(), expected_policies.len()); + expected_policies.iter().for_each(|p| assert!(policies.contains(p))); + } +} diff --git a/bin/sozo/tests/test_data/policies.json b/bin/sozo/tests/test_data/policies.json new file mode 100644 index 0000000000..d21b20abfb --- /dev/null +++ b/bin/sozo/tests/test_data/policies.json @@ -0,0 +1,126 @@ +[ + { + "target": "0x2d24481107b55ecd73c4d1b62f6bfe8c42a224447b71db7dcec2eab484d53cd", + "method": "spawn" + }, + { + "target": "0x2d24481107b55ecd73c4d1b62f6bfe8c42a224447b71db7dcec2eab484d53cd", + "method": "move" + }, + { + "target": "0x2d24481107b55ecd73c4d1b62f6bfe8c42a224447b71db7dcec2eab484d53cd", + "method": "set_player_config" + }, + { + "target": "0x2d24481107b55ecd73c4d1b62f6bfe8c42a224447b71db7dcec2eab484d53cd", + "method": "update_player_name" + }, + { + "target": "0x2d24481107b55ecd73c4d1b62f6bfe8c42a224447b71db7dcec2eab484d53cd", + "method": "update_player_name_value" + }, + { + "target": "0x2d24481107b55ecd73c4d1b62f6bfe8c42a224447b71db7dcec2eab484d53cd", + "method": "reset_player_config" + }, + { + "target": "0x2d24481107b55ecd73c4d1b62f6bfe8c42a224447b71db7dcec2eab484d53cd", + "method": "set_player_server_profile" + }, + { + "target": "0x2d24481107b55ecd73c4d1b62f6bfe8c42a224447b71db7dcec2eab484d53cd", + "method": "enter_dungeon" + }, + { + "target": "0x2d24481107b55ecd73c4d1b62f6bfe8c42a224447b71db7dcec2eab484d53cd", + "method": "upgrade" + }, + { + "target": "0x454e4731e29aad869794ce03040f1bd866556132b0e633a376918ee17801f5e", + "method": "upgrade" + }, + { + "target": "0x57d20e85621372042af6b626884361c1c64c701b0b7db985d10faf92aa0dedc", + "method": "upgrade" + }, + { + "target": "0x52da0b3df1cb3f0627dbe75960ae5ebad647b6ade1930dc9a499c0475168754", + "method": "upgrade" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "set_metadata" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "register_model" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "register_namespace" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "deploy_contract" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "upgrade_contract" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "uuid" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "set_entity" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "delete_entity" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "grant_owner" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "revoke_owner" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "grant_writer" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "revoke_writer" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "upgrade" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "upgrade_state" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "set_differ_program_hash" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "set_merger_program_hash" + }, + { + "target": "0x74c73d35df54ddc53bcf34aab5e0dbb09c447e99e01f4d69535441253c9571a", + "method": "set_facts_registry" + }, + { + "target": "0x6162896d1d7ab204c7ccac6dd5f8e9e7c25ecd5ae4fcb4ad32e57786bb46e03", + "method": "__declare_transaction__" + }, + { + "target": "0x41a78e741e5af2fec34b695679bc6891742439f7afb8484ecd7766661ad02bf", + "method": "deployContract" + } +]