From a5f84ba6672c30def838589670a1f49892ec0411 Mon Sep 17 00:00:00 2001 From: glihm Date: Wed, 26 Nov 2025 12:41:32 -0600 Subject: [PATCH 1/4] feat: add declare and deploy to sozo --- Cargo.lock | 2 + bin/sozo/Cargo.toml | 2 + bin/sozo/src/commands/declare.rs | 159 +++++++++++++++++++ bin/sozo/src/commands/deploy.rs | 142 +++++++++++++++++ bin/sozo/src/commands/mod.rs | 12 ++ bin/sozo/src/commands/options/account/mod.rs | 105 +++++++++++- bin/sozo/src/commands/options/signer.rs | 12 ++ bin/sozo/src/commands/options/starknet.rs | 22 +++ bin/sozo/src/main.rs | 4 + bin/sozo/src/utils.rs | 11 ++ 10 files changed, 469 insertions(+), 2 deletions(-) create mode 100644 bin/sozo/src/commands/declare.rs create mode 100644 bin/sozo/src/commands/deploy.rs diff --git a/Cargo.lock b/Cargo.lock index de1f848a5c..58f926896f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7175,6 +7175,7 @@ dependencies = [ "anyhow", "async-trait", "cainome", + "cairo-lang-starknet-classes", "camino", "clap", "clap-verbosity-flag", @@ -7201,6 +7202,7 @@ dependencies = [ "sozo-walnut", "starknet", "starknet-crypto 0.8.1 (git+https://github.com/dojoengine/starknet-rs?branch=feat%2Ftypes-rs-100-blake2s)", + "starknet_api", "tabled", "terminal-colorsaurus", "thiserror 1.0.69", diff --git a/bin/sozo/Cargo.toml b/bin/sozo/Cargo.toml index f0469adc10..b39510ae32 100644 --- a/bin/sozo/Cargo.toml +++ b/bin/sozo/Cargo.toml @@ -42,6 +42,8 @@ tracing.workspace = true tracing-log.workspace = true tracing-subscriber.workspace = true url.workspace = true +cairo-lang-starknet-classes.workspace = true +starknet_api.workspace = true reqwest = { workspace = true, features = [ "json" ] } diff --git a/bin/sozo/src/commands/declare.rs b/bin/sozo/src/commands/declare.rs new file mode 100644 index 0000000000..2c8ce579e8 --- /dev/null +++ b/bin/sozo/src/commands/declare.rs @@ -0,0 +1,159 @@ +use std::fs; +use std::path::{Path, PathBuf}; + +use anyhow::{anyhow, Context, Result}; +use cairo_lang_starknet_classes::casm_contract_class::CasmContractClass; +use cairo_lang_starknet_classes::contract_class::ContractClass; +use clap::Args; +use dojo_utils::{Declarer, LabeledClass, TransactionResult, TxnConfig}; +use sozo_ui::SozoUi; +use starknet::core::types::contract::SierraClass; +use starknet::core::types::{Felt, FlattenedSierraClass}; +use starknet_api::contract_class::compiled_class_hash::{HashVersion, HashableCompiledClass}; +use tracing::trace; + +use super::options::account::AccountOptions; +use super::options::starknet::StarknetOptions; +use super::options::transaction::TransactionOptions; +use crate::utils::get_account_from_env; + +#[derive(Debug, Args)] +#[command(about = "Declare one or more Sierra contracts by compiling them to CASM and sending \ + declare transactions.")] +pub struct DeclareArgs { + #[arg( + value_name = "SIERRA_PATH", + num_args = 1.., + help = "Path(s) to Sierra contract JSON artifacts (.contract_class.json)." + )] + pub contracts: Vec, + + #[command(flatten)] + pub transaction: TransactionOptions, + + #[command(flatten)] + pub starknet: StarknetOptions, + + #[command(flatten)] + pub account: AccountOptions, +} + +impl DeclareArgs { + pub async fn run(self, ui: &SozoUi) -> Result<()> { + trace!(args = ?self); + + let DeclareArgs { contracts, transaction, starknet, account } = self; + + if contracts.is_empty() { + return Err(anyhow!("At least one Sierra artifact path must be provided.")); + } + + // TODO: allow cutom headers form the CLI. + let headers = None; + let account = get_account_from_env(account, &starknet, headers).await?; + + // TODO: maybe add the detection from the provider URL too. + let use_blake2s = starknet.use_blake2s_casm_class_hash; + + let txn_config: TxnConfig = transaction.try_into()?; + + ui.title("Declare contracts"); + + let mut prepared = Vec::new(); + for path in contracts { + let class = prepare_class(&path, use_blake2s) + .with_context(|| format!("Failed to prepare Sierra artifact {}", path.display()))?; + + ui.step(format!("Compiled '{}'", class.label)); + let detail_ui = ui.subsection(); + detail_ui.verbose(format!("Class hash : {:#066x}", class.class_hash)); + detail_ui.verbose(format!("CASM hash : {:#066x}", class.casm_class_hash)); + detail_ui.verbose(format!("Artifact : {}", path.display())); + + prepared.push(class); + } + + let labeled = prepared + .iter() + .map(|class| LabeledClass { + label: class.label.clone(), + casm_class_hash: class.casm_class_hash, + class: class.class.clone(), + }) + .collect::>(); + + let mut declarer = Declarer::new(account, txn_config); + declarer.extend_classes(labeled); + + let results = declarer.declare_all().await?; + + let mut declared = 0usize; + for (class, result) in prepared.iter().zip(results.iter()) { + match result { + TransactionResult::Noop => { + ui.verbose( + ui.indent(1, format!("'{}' already declared on-chain.", class.label)), + ); + ui.verbose(ui.indent(2, format!("Class hash: {:#066x}", class.class_hash))); + } + TransactionResult::Hash(hash) => { + declared += 1; + ui.result(ui.indent(1, format!("'{}' declared.", class.label))); + ui.verbose(ui.indent(2, format!("Class hash: {:#066x}", class.class_hash))); + ui.verbose(ui.indent(2, format!("Tx hash : {hash:#066x}"))); + } + TransactionResult::HashReceipt(hash, receipt) => { + declared += 1; + ui.result(ui.indent(1, format!("'{}' declared.", class.label))); + ui.verbose(ui.indent(2, format!("Class hash: {:#066x}", class.class_hash))); + ui.verbose(ui.indent(2, format!("Tx hash : {hash:#066x}"))); + ui.debug(format!("Receipt: {:?}", receipt)); + } + } + } + + if declared == 0 { + ui.result("All provided classes are already declared."); + } else { + ui.result(format!("Declared {} class(es).", declared)); + } + + Ok(()) + } +} + +#[derive(Debug)] +struct PreparedClass { + label: String, + class_hash: Felt, + casm_class_hash: Felt, + class: FlattenedSierraClass, +} + +fn prepare_class(path: &Path, use_blake2s: bool) -> Result { + let data = fs::read(path)?; + + let sierra: SierraClass = serde_json::from_slice(&data)?; + let class_hash = sierra.class_hash()?; + let flattened = sierra.clone().flatten()?; + + let casm_hash = casm_class_hash_from_bytes(&data, use_blake2s)?; + + let label = path + .file_stem() + .and_then(|stem| stem.to_str()) + .ok_or_else(|| anyhow!("Unable to infer contract name from {}", path.display()))? + .to_string(); + + Ok(PreparedClass { label, class_hash, casm_class_hash: casm_hash, class: flattened }) +} + +fn casm_class_hash_from_bytes(data: &[u8], use_blake2s: bool) -> Result { + let sierra_class: ContractClass = serde_json::from_slice(data)?; + let casm_class = CasmContractClass::from_contract_class(sierra_class, false, usize::MAX)?; + + let hash_version = if use_blake2s { HashVersion::V2 } else { HashVersion::V1 }; + let hash = casm_class.hash(&hash_version); + + Ok(Felt::from_bytes_be(&hash.0.to_bytes_be())) +} diff --git a/bin/sozo/src/commands/deploy.rs b/bin/sozo/src/commands/deploy.rs new file mode 100644 index 0000000000..0ef716c0fa --- /dev/null +++ b/bin/sozo/src/commands/deploy.rs @@ -0,0 +1,142 @@ +use anyhow::{Context, Result}; +use clap::Args; +use dojo_utils::{Deployer, TransactionResult, TxnConfig}; +use dojo_world::config::calldata_decoder::decode_calldata; +use sozo_ui::SozoUi; +use starknet::core::types::Felt; +use starknet::core::utils::get_contract_address; +use tracing::trace; + +use super::options::account::AccountOptions; +use super::options::starknet::StarknetOptions; +use super::options::transaction::TransactionOptions; +use crate::utils::{get_account_from_env, CALLDATA_DOC}; + +#[derive(Debug, Args)] +#[command(about = "Deploy a declared class through the Universal Deployer Contract (UDC).")] +pub struct DeployArgs { + #[arg(value_name = "CLASS_HASH", help = "The class hash to deploy.")] + pub class_hash: Felt, + + #[arg(long, default_value = "0x0", help = "Salt to use for the deployment.")] + pub salt: Felt, + + #[arg( + long, + default_value = "0x0", + help = "Deployer address to pass to the UDC. Defaults to zero for standard deployments." + )] + pub deployer_address: Felt, + + #[arg( + long = "constructor-calldata", + value_name = "ARG", + num_args = 0.., + help = format!( + "Constructor calldata elements (space separated).\n\n{}", + CALLDATA_DOC + ) + )] + pub constructor_calldata: Vec, + + #[command(flatten)] + pub transaction: TransactionOptions, + + #[command(flatten)] + pub starknet: StarknetOptions, + + #[command(flatten)] + pub account: AccountOptions, +} + +impl DeployArgs { + pub async fn run(self, ui: &SozoUi) -> Result<()> { + trace!(args = ?self); + + let DeployArgs { + class_hash, + salt, + deployer_address, + constructor_calldata, + transaction, + starknet, + account, + } = self; + + let constructor_felts = decode_calldata(&constructor_calldata) + .context("Failed to parse constructor calldata")?; + let expected_address = + get_contract_address(salt, class_hash, &constructor_felts, deployer_address); + + let txn_config: TxnConfig = transaction.try_into()?; + + // TODO: allow custom headers from the CLI. + let headers = None; + let account = get_account_from_env(account, &starknet, headers).await?; + + ui.title("Deploy contract"); + ui.step("Deploying contract via UDC"); + let params_ui = ui.subsection(); + params_ui.verbose(format!("Class hash : {:#066x}", class_hash)); + params_ui.verbose(format!("Salt : {salt:#066x}")); + params_ui.verbose(format!("Deployer : {deployer_address:#066x}")); + params_ui.verbose(format!("Expect addr: {expected_address:#066x}")); + if constructor_felts.is_empty() { + params_ui.verbose("Constructor calldata: "); + } else { + params_ui.verbose(format!("Constructor felts ({})", constructor_felts.len())); + for (idx, value) in constructor_felts.iter().enumerate() { + params_ui.verbose(params_ui.indent(1, format!("[{idx}] {value:#066x}"))); + } + } + + let deployer = Deployer::new(account, txn_config); + let (actual_address, tx_result) = + deployer.deploy_via_udc(class_hash, salt, &constructor_felts, deployer_address).await?; + + match tx_result { + TransactionResult::Noop => { + let address = + if actual_address == Felt::ZERO { expected_address } else { actual_address }; + ui.result(format!("Contract already deployed.\n Address : {address:#066x}")); + if address != expected_address { + ui.warn(format!( + "Computed address {expected_address:#066x} differs from on-chain \ + {address:#066x}." + )); + } + } + TransactionResult::Hash(hash) => { + let deployed = + if actual_address == Felt::ZERO { expected_address } else { actual_address }; + ui.result(format!( + "Deployment submitted.\n Tx hash : {hash:#066x}\n Address : \ + {deployed:#066x}" + )); + if deployed != expected_address { + ui.warn(format!( + "Computed address {expected_address:#066x} differs from on-chain \ + {deployed:#066x}." + )); + } + } + TransactionResult::HashReceipt(hash, receipt) => { + let deployed = + if actual_address == Felt::ZERO { expected_address } else { actual_address }; + ui.result(format!( + "Deployment included on-chain.\n Tx hash : {hash:#066x}\n Address : \ + {deployed:#066x}" + )); + if deployed != expected_address { + ui.warn(format!( + "Computed address {expected_address:#066x} differs from on-chain \ + {deployed:#066x}." + )); + } + ui.debug(format!("Receipt: {:?}", receipt)); + } + } + + Ok(()) + } +} diff --git a/bin/sozo/src/commands/mod.rs b/bin/sozo/src/commands/mod.rs index 77fc3d32e5..fccbe84e6c 100644 --- a/bin/sozo/src/commands/mod.rs +++ b/bin/sozo/src/commands/mod.rs @@ -11,6 +11,8 @@ pub(crate) mod bindgen; pub(crate) mod build; pub(crate) mod call; pub(crate) mod clean; +pub(crate) mod declare; +pub(crate) mod deploy; pub(crate) mod events; pub(crate) mod execute; pub(crate) mod hash; @@ -28,6 +30,8 @@ use bindgen::BindgenArgs; use build::BuildArgs; use call::CallArgs; use clean::CleanArgs; +use declare::DeclareArgs; +use deploy::DeployArgs; use events::EventsArgs; use execute::ExecuteArgs; use hash::HashArgs; @@ -59,6 +63,10 @@ pub enum Commands { Clean(Box), #[command(about = "Computes hash with different hash functions")] Hash(Box), + #[command(about = "Declare Dojo contracts and Sozo-managed classes on Starknet")] + Declare(Box), + #[command(about = "Deploy Dojo world or Sozo-managed contracts")] + Deploy(Box), #[command(about = "Initialize a new dojo project")] Init(Box), #[command(about = "Inspect the world")] @@ -90,6 +98,8 @@ impl fmt::Display for Commands { Commands::Events(_) => write!(f, "Events"), Commands::Execute(_) => write!(f, "Execute"), Commands::Hash(_) => write!(f, "Hash"), + Commands::Declare(_) => write!(f, "Declare"), + Commands::Deploy(_) => write!(f, "Deploy"), Commands::Init(_) => write!(f, "Init"), Commands::Inspect(_) => write!(f, "Inspect"), Commands::Migrate(_) => write!(f, "Migrate"), @@ -117,6 +127,8 @@ pub async fn run(command: Commands, scarb_metadata: &Metadata, ui: &SozoUi) -> R Commands::Events(args) => args.run(scarb_metadata, ui).await, Commands::Execute(args) => args.run(scarb_metadata, ui).await, Commands::Hash(args) => args.run(scarb_metadata), + Commands::Declare(args) => args.run(ui).await, + Commands::Deploy(args) => args.run(ui).await, Commands::Inspect(args) => args.run(scarb_metadata, ui).await, Commands::Mcp(args) => args.run(scarb_metadata).await, Commands::Migrate(args) => args.run(scarb_metadata, ui).await, diff --git a/bin/sozo/src/commands/options/account/mod.rs b/bin/sozo/src/commands/options/account/mod.rs index c652a0394d..6c18d698bb 100644 --- a/bin/sozo/src/commands/options/account/mod.rs +++ b/bin/sozo/src/commands/options/account/mod.rs @@ -3,7 +3,7 @@ use std::str::FromStr; use std::sync::Arc; use anyhow::{anyhow, Result}; -use clap::Args; +use clap::{Args, ValueEnum}; use dojo_utils::env::DOJO_ACCOUNT_ADDRESS_ENV_VAR; use dojo_world::config::Environment; use dojo_world::contracts::ContractInfo; @@ -39,6 +39,12 @@ pub struct AccountOptions { #[arg(global = true)] pub account_address: Option, + #[arg(long, global = true)] + #[arg(value_enum)] + #[arg(value_name = "NAME")] + #[arg(help = "Use one of Katana's pre-funded dev accounts (katana0..katana9).")] + pub katana_account: Option, + #[arg(global = true)] #[arg(long = "slot.controller")] #[arg(help_heading = "Controller options")] @@ -119,7 +125,18 @@ impl AccountOptions { { let account_address = self.account_address(env_metadata)?; - let signer = self.signer.signer(env_metadata, false)?; + let katana_details = self.katana_details(); + let signer_options = if let Some(details) = katana_details { + if !self.signer.has_custom_signer() { + self.signer.with_private_key(details.private_key) + } else { + self.signer.clone() + } + } else { + self.signer.clone() + }; + + let signer = signer_options.signer(env_metadata, false)?; trace!("Fetching chain id..."); let chain_id = provider.chain_id().await?; @@ -140,6 +157,9 @@ impl AccountOptions { if let Some(address) = self.account_address { trace!(?address, "Account address found."); Ok(address) + } else if let Some(details) = self.katana_details() { + trace!(address = details.address, "Using Katana preset account address."); + Ok(Felt::from_str(details.address)?) } else if let Some(address) = env_metadata.and_then(|env| env.account_address()) { trace!(address, "Account address found in environment metadata."); Ok(Felt::from_str(address)?) @@ -150,6 +170,10 @@ impl AccountOptions { )) } } + + fn katana_details(&self) -> Option { + self.katana_account.map(|preset| preset.details()) + } } #[cfg(test)] @@ -261,3 +285,80 @@ mod tests { assert!(*result.get(3).unwrap() == Felt::from_hex("0x2").unwrap()); } } + +#[derive(Clone, Copy)] +struct KatanaAccountDetails { + address: &'static str, + private_key: &'static str, +} + +#[derive(Clone, Copy, Debug, ValueEnum)] +pub enum KatanaAccount { + #[value(name = "katana0")] + Katana0, + #[value(name = "katana1")] + Katana1, + #[value(name = "katana2")] + Katana2, + #[value(name = "katana3")] + Katana3, + #[value(name = "katana4")] + Katana4, + #[value(name = "katana5")] + Katana5, + #[value(name = "katana6")] + Katana6, + #[value(name = "katana7")] + Katana7, + #[value(name = "katana8")] + Katana8, + #[value(name = "katana9")] + Katana9, +} + +impl KatanaAccount { + fn details(self) -> KatanaAccountDetails { + match self { + KatanaAccount::Katana0 => KatanaAccountDetails { + address: "0x127fd5f1fe78a71f8bcd1fec63e3fe2f0486b6ecd5c86a0466c3a21fa5cfcec", + private_key: "0x00c5b2fcab997346f3ea1c00b002ecf6f382c5f9c9659a3894eb783c5320f912", + }, + KatanaAccount::Katana1 => KatanaAccountDetails { + address: "0x13d9ee239f33fea4f8785b9e3870ade909e20a9599ae7cd62c1c292b73af1b7", + private_key: "0x01c9053c053edf324aec366a34c6901b1095b07af69495bffec7d7fe21effb1b", + }, + KatanaAccount::Katana2 => KatanaAccountDetails { + address: "0x17cc6ca902ed4e8baa8463a7009ff18cc294fa85a94b4ce6ac30a9ebd6057c7", + private_key: "0x014d6672dcb4b77ca36a887e9a11cd9d637d5012468175829e9c6e770c61642", + }, + KatanaAccount::Katana3 => KatanaAccountDetails { + address: "0x2af9427c5a277474c079a1283c880ee8a6f0f8fbf73ce969c08d88befec1bba", + private_key: "0x018000000003000001800000000000300000000000003006001800006600", + }, + KatanaAccount::Katana4 => KatanaAccountDetails { + address: "0x359b9068eadcaaa449c08b79a367c6fdfba9448c29e96934e3552dab0fdd950", + private_key: "0x02bbf4f9fd0bbb2e60b0316c1fe0b76cf7a4d0198bd493ced9b8df2a3a24d68a", + }, + KatanaAccount::Katana5 => KatanaAccountDetails { + address: "0x4184158a64a82eb982ff702e4041a49db16fa3a18229aac4ce88c832baf56e4", + private_key: "0x06bf3604bcb41fed6c42bcca5436eeb65083a982ff65db0dc123f65358008b51", + }, + KatanaAccount::Katana6 => KatanaAccountDetails { + address: "0x42b249d1633812d903f303d640a4261f58fead5aa24925a9efc1dd9d76fb555", + private_key: "0x0283d1e73776cd4ac1ac5f0b879f561bded25eceb2cc589c674af0cec41df441", + }, + KatanaAccount::Katana7 => KatanaAccountDetails { + address: "0x4e0b838810cb1a355beb7b3d894ca0e98ee524309c3f8b7cccb15a48e6270e2", + private_key: "0x0736adbbcdac7cc600f89051db1abbc16b9996b46f6b58a9752a11c1028a8ec8", + }, + KatanaAccount::Katana8 => KatanaAccountDetails { + address: "0x5b6b8189bb580f0df1e6d6bec509ff0d6c9be7365d10627e0cf222ec1b47a71", + private_key: "0x0330030030018000099001803000d206308b0070db00121318d17b5e6262150b", + }, + KatanaAccount::Katana9 => KatanaAccountDetails { + address: "0x6677fe62ee39c7b07401f754138502bab7fac99d2d3c5d37df7d1c6fab10819", + private_key: "0x03e3979c1ed728490308054fe357a9f49cf67f80f9721f44cc57235129e090f4", + }, + } + } +} diff --git a/bin/sozo/src/commands/options/signer.rs b/bin/sozo/src/commands/options/signer.rs index 707ec8d897..5e7694cb91 100644 --- a/bin/sozo/src/commands/options/signer.rs +++ b/bin/sozo/src/commands/options/signer.rs @@ -46,6 +46,18 @@ pub struct SignerOptions { } impl SignerOptions { + pub fn with_private_key(&self, private_key: &str) -> Self { + let mut cloned = self.clone(); + cloned.private_key = Some(private_key.to_owned()); + cloned.keystore_path = None; + cloned.keystore_password = None; + cloned + } + + pub fn has_custom_signer(&self) -> bool { + self.private_key.is_some() || self.keystore_path.is_some() + } + /// Retrieves the signer from the CLI or environment metadata. /// First, attempt to locate the signer from CLI arguments or environment variables via CLAP. /// If unsuccessful, then search for the signer within the Dojo environment metadata. diff --git a/bin/sozo/src/commands/options/starknet.rs b/bin/sozo/src/commands/options/starknet.rs index 91ea595f3d..f013593ccd 100644 --- a/bin/sozo/src/commands/options/starknet.rs +++ b/bin/sozo/src/commands/options/starknet.rs @@ -57,6 +57,28 @@ impl StarknetOptions { Ok((JsonRpcClient::new(transport), url.to_string())) } + /// Returns a [`JsonRpcClient`] from the given rpc url and headers. + pub fn provider_from_url( + &self, + headers: Option>, + ) -> Result> { + let client = + ClientBuilder::default().timeout(Self::DEFAULT_REQUEST_TIMEOUT).build().unwrap(); + + let mut transport = HttpTransport::new_with_client( + self.rpc_url.clone().unwrap_or(Url::parse("http://localhost:5050").unwrap()), + client, + ); + + if let Some(headers) = headers { + for header in headers.iter() { + transport.add_header(header.0.clone(), header.1.clone()); + } + } + + Ok(JsonRpcClient::new(transport)) + } + // We dont check the env var because that would be handled by `clap`. // This function is made public because [`JsonRpcClient`] does not expose // the raw rpc url. diff --git a/bin/sozo/src/main.rs b/bin/sozo/src/main.rs index 2859d485ca..34f3c42ff1 100644 --- a/bin/sozo/src/main.rs +++ b/bin/sozo/src/main.rs @@ -43,6 +43,10 @@ async fn main() { async fn cli_main(args: SozoArgs, ui: &SozoUi) -> Result<()> { if let Commands::Init(args) = args.command { args.run(ui) + } else if let Commands::Declare(args) = args.command { + args.run(ui).await + } else if let Commands::Deploy(args) = args.command { + args.run(ui).await } else { // Default to the current directory to mimic how Scarb works. let manifest_path = if let Some(manifest_path) = &args.manifest_path { diff --git a/bin/sozo/src/utils.rs b/bin/sozo/src/utils.rs index 59ab6db8ec..24611e9c86 100644 --- a/bin/sozo/src/utils.rs +++ b/bin/sozo/src/utils.rs @@ -186,6 +186,17 @@ pub async fn get_world_diff_and_account( Ok((world_diff, account, rpc_url)) } +// Gets the account from the environment. +pub async fn get_account_from_env( + account: AccountOptions, + starknet: &StarknetOptions, + headers: Option>, +) -> Result>> { + let provider = starknet.provider_from_url(headers)?; + + account.account(provider, None, starknet, &HashMap::new()).await +} + fn show_profile_details(profile_config: &ProfileConfig, ui: &SozoUi) { ui.verbose("local profile"); let local_ui = ui.subsection(); From 66b5b671e76e557d8775812814abe3a0b2332dbc Mon Sep 17 00:00:00 2001 From: glihm Date: Wed, 26 Nov 2025 15:03:08 -0600 Subject: [PATCH 2/4] fix: add headers + auto-blake detection --- bin/sozo/src/commands/declare.rs | 15 ++++--- bin/sozo/src/commands/deploy.rs | 4 +- bin/sozo/src/commands/options/starknet.rs | 52 +++++++++++++++++++++-- bin/sozo/src/utils.rs | 3 +- 4 files changed, 61 insertions(+), 13 deletions(-) diff --git a/bin/sozo/src/commands/declare.rs b/bin/sozo/src/commands/declare.rs index 2c8ce579e8..5790af2539 100644 --- a/bin/sozo/src/commands/declare.rs +++ b/bin/sozo/src/commands/declare.rs @@ -48,12 +48,17 @@ impl DeclareArgs { return Err(anyhow!("At least one Sierra artifact path must be provided.")); } - // TODO: allow cutom headers form the CLI. - let headers = None; - let account = get_account_from_env(account, &starknet, headers).await?; + let account = get_account_from_env(account, &starknet).await?; - // TODO: maybe add the detection from the provider URL too. - let use_blake2s = starknet.use_blake2s_casm_class_hash; + let use_blake2s = if let Some(rpc_url) = starknet.rpc_url { + if rpc_url.to_string().contains("sepolia") || rpc_url.to_string().contains("testnet") { + true + } else { + starknet.use_blake2s_casm_class_hash + } + } else { + starknet.use_blake2s_casm_class_hash + }; let txn_config: TxnConfig = transaction.try_into()?; diff --git a/bin/sozo/src/commands/deploy.rs b/bin/sozo/src/commands/deploy.rs index 0ef716c0fa..9cdd7253e1 100644 --- a/bin/sozo/src/commands/deploy.rs +++ b/bin/sozo/src/commands/deploy.rs @@ -70,9 +70,7 @@ impl DeployArgs { let txn_config: TxnConfig = transaction.try_into()?; - // TODO: allow custom headers from the CLI. - let headers = None; - let account = get_account_from_env(account, &starknet, headers).await?; + let account = get_account_from_env(account, &starknet).await?; ui.title("Deploy contract"); ui.step("Deploying contract via UDC"); diff --git a/bin/sozo/src/commands/options/starknet.rs b/bin/sozo/src/commands/options/starknet.rs index f013593ccd..6847a30adc 100644 --- a/bin/sozo/src/commands/options/starknet.rs +++ b/bin/sozo/src/commands/options/starknet.rs @@ -1,6 +1,8 @@ +use std::fmt::{Display, Formatter}; +use std::str::FromStr; use std::time::Duration; -use anyhow::Result; +use anyhow::{anyhow, Result}; use clap::Args; use dojo_utils::env::STARKNET_RPC_URL_ENV_VAR; use dojo_world::config::Environment; @@ -26,6 +28,12 @@ pub struct StarknetOptions { Otherwise, use this flag to manually set it.")] #[arg(global = true)] pub use_blake2s_casm_class_hash: bool, + + #[arg(long = "rpc-header")] + #[arg(value_name = "HEADER")] + #[arg(help = "Custom header(s) to add to Starknet RPC requests. Format: Name:Value")] + #[arg(global = true)] + pub rpc_headers: Vec, } impl StarknetOptions { @@ -54,6 +62,10 @@ impl StarknetOptions { } } + for header in &self.rpc_headers { + transport.add_header(header.name.clone(), header.value.clone()); + } + Ok((JsonRpcClient::new(transport), url.to_string())) } @@ -70,9 +82,13 @@ impl StarknetOptions { client, ); + for header in &self.rpc_headers { + transport.add_header(header.name.clone(), header.value.clone()); + } + if let Some(headers) = headers { - for header in headers.iter() { - transport.add_header(header.0.clone(), header.1.clone()); + for header in headers.into_iter() { + transport.add_header(header.0, header.1); } } @@ -97,6 +113,36 @@ impl StarknetOptions { } } +#[derive(Clone, Debug)] +pub struct RpcHeader { + pub name: String, + pub value: String, +} + +impl FromStr for RpcHeader { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let (name, value) = + s.split_once(':').ok_or_else(|| anyhow!("Invalid header format. Use `Name:Value`"))?; + + let name = name.trim(); + let value = value.trim(); + + if name.is_empty() || value.is_empty() { + return Err(anyhow!("Header name and value must be non-empty.")); + } + + Ok(RpcHeader { name: name.to_string(), value: value.to_string() }) + } +} + +impl Display for RpcHeader { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}: {}", self.name, self.value) + } +} + #[cfg(test)] mod tests { use clap::Parser; diff --git a/bin/sozo/src/utils.rs b/bin/sozo/src/utils.rs index 24611e9c86..d7b9b654d2 100644 --- a/bin/sozo/src/utils.rs +++ b/bin/sozo/src/utils.rs @@ -190,9 +190,8 @@ pub async fn get_world_diff_and_account( pub async fn get_account_from_env( account: AccountOptions, starknet: &StarknetOptions, - headers: Option>, ) -> Result>> { - let provider = starknet.provider_from_url(headers)?; + let provider = starknet.provider_from_url(None)?; account.account(provider, None, starknet, &HashMap::new()).await } From 752c5f4bf944ef686b11bf8d922a2d5041d1c158 Mon Sep 17 00:00:00 2001 From: glihm Date: Wed, 26 Nov 2025 15:45:30 -0600 Subject: [PATCH 3/4] fix display of addresses and check priority of accounts --- bin/sozo/src/commands/declare.rs | 10 ++++++---- bin/sozo/src/commands/deploy.rs | 4 ++-- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/bin/sozo/src/commands/declare.rs b/bin/sozo/src/commands/declare.rs index 5790af2539..f53d7fb25e 100644 --- a/bin/sozo/src/commands/declare.rs +++ b/bin/sozo/src/commands/declare.rs @@ -69,9 +69,8 @@ impl DeclareArgs { let class = prepare_class(&path, use_blake2s) .with_context(|| format!("Failed to prepare Sierra artifact {}", path.display()))?; - ui.step(format!("Compiled '{}'", class.label)); + ui.step(format!("Compiled '{}' (class hash {:#066x})", class.label, class.class_hash)); let detail_ui = ui.subsection(); - detail_ui.verbose(format!("Class hash : {:#066x}", class.class_hash)); detail_ui.verbose(format!("CASM hash : {:#066x}", class.casm_class_hash)); detail_ui.verbose(format!("Artifact : {}", path.display())); @@ -145,8 +144,11 @@ fn prepare_class(path: &Path, use_blake2s: bool) -> Result { let casm_hash = casm_class_hash_from_bytes(&data, use_blake2s)?; let label = path - .file_stem() - .and_then(|stem| stem.to_str()) + .file_name() + .and_then(|name| name.to_str()) + .ok_or_else(|| anyhow!("Unable to infer contract name from {}", path.display()))? + .split('.') + .next() .ok_or_else(|| anyhow!("Unable to infer contract name from {}", path.display()))? .to_string(); diff --git a/bin/sozo/src/commands/deploy.rs b/bin/sozo/src/commands/deploy.rs index 9cdd7253e1..3d897abc8e 100644 --- a/bin/sozo/src/commands/deploy.rs +++ b/bin/sozo/src/commands/deploy.rs @@ -72,7 +72,7 @@ impl DeployArgs { let account = get_account_from_env(account, &starknet).await?; - ui.title("Deploy contract"); + ui.title(format!("Deploy contract (class hash {:#066x})", class_hash)); ui.step("Deploying contract via UDC"); let params_ui = ui.subsection(); params_ui.verbose(format!("Class hash : {:#066x}", class_hash)); @@ -122,7 +122,7 @@ impl DeployArgs { let deployed = if actual_address == Felt::ZERO { expected_address } else { actual_address }; ui.result(format!( - "Deployment included on-chain.\n Tx hash : {hash:#066x}\n Address : \ + "Contract deployed onchain.\n Tx hash : {hash:#066x}\n Address : \ {deployed:#066x}" )); if deployed != expected_address { From 5ea47fe831fc04c6c58539b35af1ac00c5040781 Mon Sep 17 00:00:00 2001 From: glihm Date: Wed, 26 Nov 2025 15:48:43 -0600 Subject: [PATCH 4/4] clean some args and display --- bin/sozo/src/commands/options/starknet.rs | 11 +---------- bin/sozo/src/utils.rs | 2 +- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/bin/sozo/src/commands/options/starknet.rs b/bin/sozo/src/commands/options/starknet.rs index 6847a30adc..edae6052e8 100644 --- a/bin/sozo/src/commands/options/starknet.rs +++ b/bin/sozo/src/commands/options/starknet.rs @@ -70,10 +70,7 @@ impl StarknetOptions { } /// Returns a [`JsonRpcClient`] from the given rpc url and headers. - pub fn provider_from_url( - &self, - headers: Option>, - ) -> Result> { + pub fn provider_from_url(&self) -> Result> { let client = ClientBuilder::default().timeout(Self::DEFAULT_REQUEST_TIMEOUT).build().unwrap(); @@ -86,12 +83,6 @@ impl StarknetOptions { transport.add_header(header.name.clone(), header.value.clone()); } - if let Some(headers) = headers { - for header in headers.into_iter() { - transport.add_header(header.0, header.1); - } - } - Ok(JsonRpcClient::new(transport)) } diff --git a/bin/sozo/src/utils.rs b/bin/sozo/src/utils.rs index d7b9b654d2..6daede5306 100644 --- a/bin/sozo/src/utils.rs +++ b/bin/sozo/src/utils.rs @@ -191,7 +191,7 @@ pub async fn get_account_from_env( account: AccountOptions, starknet: &StarknetOptions, ) -> Result>> { - let provider = starknet.provider_from_url(None)?; + let provider = starknet.provider_from_url()?; account.account(provider, None, starknet, &HashMap::new()).await }