Skip to content

Commit

Permalink
refactor(sozo): deploy controller account if not exist (#2242)
Browse files Browse the repository at this point in the history
  • Loading branch information
kariy authored Aug 1, 2024
1 parent 665ef43 commit 1b75ac9
Show file tree
Hide file tree
Showing 4 changed files with 250 additions and 13 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion bin/sozo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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" ]
133 changes: 121 additions & 12 deletions bin/sozo/src/commands/options/account/controller.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)?;

Expand All @@ -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 {
Expand All @@ -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
}
Expand All @@ -103,6 +107,11 @@ where
let expires_at = session_details.expires_at.parse::<u64>()?;
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,
Expand Down Expand Up @@ -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::<Vec<AbiEntry>>(&abis)?;
policies_from_abis(&mut policies, &contract.inner.tag, contract_address, &abis);
Expand Down Expand Up @@ -209,12 +218,24 @@ fn policies_from_abis(
}
}

fn get_dojo_contract_address(world_address: Felt, manifest: &Manifest<DojoContract>) -> Felt {
if let Some(address) = manifest.inner.address {
fn get_dojo_contract_address(
world_address: Felt,
contract: &Manifest<DojoContract>,
base_class: &Manifest<Class>,
) -> 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)
}
}

Expand All @@ -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<Policy> = 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)));
}
}
126 changes: 126 additions & 0 deletions bin/sozo/tests/test_data/policies.json
Original file line number Diff line number Diff line change
@@ -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"
}
]

0 comments on commit 1b75ac9

Please sign in to comment.