Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

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

2 changes: 2 additions & 0 deletions bin/sozo/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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" ] }

Expand Down
164 changes: 164 additions & 0 deletions bin/sozo/src/commands/declare.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
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<PathBuf>,

#[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."));
}

let account = get_account_from_env(account, &starknet).await?;

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()?;

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::<Vec<_>>();

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<PreparedClass> {
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<Felt> {
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()))
}
140 changes: 140 additions & 0 deletions bin/sozo/src/commands/deploy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
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<String>,

#[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()?;

let account = get_account_from_env(account, &starknet).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: <empty>");
} 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(())
}
}
12 changes: 12 additions & 0 deletions bin/sozo/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -59,6 +63,10 @@ pub enum Commands {
Clean(Box<CleanArgs>),
#[command(about = "Computes hash with different hash functions")]
Hash(Box<HashArgs>),
#[command(about = "Declare Dojo contracts and Sozo-managed classes on Starknet")]
Declare(Box<DeclareArgs>),
#[command(about = "Deploy Dojo world or Sozo-managed contracts")]
Deploy(Box<DeployArgs>),
#[command(about = "Initialize a new dojo project")]
Init(Box<InitArgs>),
#[command(about = "Inspect the world")]
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading