Skip to content

Commit a5f84ba

Browse files
committed
feat: add declare and deploy to sozo
1 parent 2bb9d20 commit a5f84ba

File tree

10 files changed

+469
-2
lines changed

10 files changed

+469
-2
lines changed

Cargo.lock

Lines changed: 2 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bin/sozo/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ tracing.workspace = true
4242
tracing-log.workspace = true
4343
tracing-subscriber.workspace = true
4444
url.workspace = true
45+
cairo-lang-starknet-classes.workspace = true
46+
starknet_api.workspace = true
4547

4648
reqwest = { workspace = true, features = [ "json" ] }
4749

bin/sozo/src/commands/declare.rs

Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
use std::fs;
2+
use std::path::{Path, PathBuf};
3+
4+
use anyhow::{anyhow, Context, Result};
5+
use cairo_lang_starknet_classes::casm_contract_class::CasmContractClass;
6+
use cairo_lang_starknet_classes::contract_class::ContractClass;
7+
use clap::Args;
8+
use dojo_utils::{Declarer, LabeledClass, TransactionResult, TxnConfig};
9+
use sozo_ui::SozoUi;
10+
use starknet::core::types::contract::SierraClass;
11+
use starknet::core::types::{Felt, FlattenedSierraClass};
12+
use starknet_api::contract_class::compiled_class_hash::{HashVersion, HashableCompiledClass};
13+
use tracing::trace;
14+
15+
use super::options::account::AccountOptions;
16+
use super::options::starknet::StarknetOptions;
17+
use super::options::transaction::TransactionOptions;
18+
use crate::utils::get_account_from_env;
19+
20+
#[derive(Debug, Args)]
21+
#[command(about = "Declare one or more Sierra contracts by compiling them to CASM and sending \
22+
declare transactions.")]
23+
pub struct DeclareArgs {
24+
#[arg(
25+
value_name = "SIERRA_PATH",
26+
num_args = 1..,
27+
help = "Path(s) to Sierra contract JSON artifacts (.contract_class.json)."
28+
)]
29+
pub contracts: Vec<PathBuf>,
30+
31+
#[command(flatten)]
32+
pub transaction: TransactionOptions,
33+
34+
#[command(flatten)]
35+
pub starknet: StarknetOptions,
36+
37+
#[command(flatten)]
38+
pub account: AccountOptions,
39+
}
40+
41+
impl DeclareArgs {
42+
pub async fn run(self, ui: &SozoUi) -> Result<()> {
43+
trace!(args = ?self);
44+
45+
let DeclareArgs { contracts, transaction, starknet, account } = self;
46+
47+
if contracts.is_empty() {
48+
return Err(anyhow!("At least one Sierra artifact path must be provided."));
49+
}
50+
51+
// TODO: allow cutom headers form the CLI.
52+
let headers = None;
53+
let account = get_account_from_env(account, &starknet, headers).await?;
54+
55+
// TODO: maybe add the detection from the provider URL too.
56+
let use_blake2s = starknet.use_blake2s_casm_class_hash;
57+
58+
let txn_config: TxnConfig = transaction.try_into()?;
59+
60+
ui.title("Declare contracts");
61+
62+
let mut prepared = Vec::new();
63+
for path in contracts {
64+
let class = prepare_class(&path, use_blake2s)
65+
.with_context(|| format!("Failed to prepare Sierra artifact {}", path.display()))?;
66+
67+
ui.step(format!("Compiled '{}'", class.label));
68+
let detail_ui = ui.subsection();
69+
detail_ui.verbose(format!("Class hash : {:#066x}", class.class_hash));
70+
detail_ui.verbose(format!("CASM hash : {:#066x}", class.casm_class_hash));
71+
detail_ui.verbose(format!("Artifact : {}", path.display()));
72+
73+
prepared.push(class);
74+
}
75+
76+
let labeled = prepared
77+
.iter()
78+
.map(|class| LabeledClass {
79+
label: class.label.clone(),
80+
casm_class_hash: class.casm_class_hash,
81+
class: class.class.clone(),
82+
})
83+
.collect::<Vec<_>>();
84+
85+
let mut declarer = Declarer::new(account, txn_config);
86+
declarer.extend_classes(labeled);
87+
88+
let results = declarer.declare_all().await?;
89+
90+
let mut declared = 0usize;
91+
for (class, result) in prepared.iter().zip(results.iter()) {
92+
match result {
93+
TransactionResult::Noop => {
94+
ui.verbose(
95+
ui.indent(1, format!("'{}' already declared on-chain.", class.label)),
96+
);
97+
ui.verbose(ui.indent(2, format!("Class hash: {:#066x}", class.class_hash)));
98+
}
99+
TransactionResult::Hash(hash) => {
100+
declared += 1;
101+
ui.result(ui.indent(1, format!("'{}' declared.", class.label)));
102+
ui.verbose(ui.indent(2, format!("Class hash: {:#066x}", class.class_hash)));
103+
ui.verbose(ui.indent(2, format!("Tx hash : {hash:#066x}")));
104+
}
105+
TransactionResult::HashReceipt(hash, receipt) => {
106+
declared += 1;
107+
ui.result(ui.indent(1, format!("'{}' declared.", class.label)));
108+
ui.verbose(ui.indent(2, format!("Class hash: {:#066x}", class.class_hash)));
109+
ui.verbose(ui.indent(2, format!("Tx hash : {hash:#066x}")));
110+
ui.debug(format!("Receipt: {:?}", receipt));
111+
}
112+
}
113+
}
114+
115+
if declared == 0 {
116+
ui.result("All provided classes are already declared.");
117+
} else {
118+
ui.result(format!("Declared {} class(es).", declared));
119+
}
120+
121+
Ok(())
122+
}
123+
}
124+
125+
#[derive(Debug)]
126+
struct PreparedClass {
127+
label: String,
128+
class_hash: Felt,
129+
casm_class_hash: Felt,
130+
class: FlattenedSierraClass,
131+
}
132+
133+
fn prepare_class(path: &Path, use_blake2s: bool) -> Result<PreparedClass> {
134+
let data = fs::read(path)?;
135+
136+
let sierra: SierraClass = serde_json::from_slice(&data)?;
137+
let class_hash = sierra.class_hash()?;
138+
let flattened = sierra.clone().flatten()?;
139+
140+
let casm_hash = casm_class_hash_from_bytes(&data, use_blake2s)?;
141+
142+
let label = path
143+
.file_stem()
144+
.and_then(|stem| stem.to_str())
145+
.ok_or_else(|| anyhow!("Unable to infer contract name from {}", path.display()))?
146+
.to_string();
147+
148+
Ok(PreparedClass { label, class_hash, casm_class_hash: casm_hash, class: flattened })
149+
}
150+
151+
fn casm_class_hash_from_bytes(data: &[u8], use_blake2s: bool) -> Result<Felt> {
152+
let sierra_class: ContractClass = serde_json::from_slice(data)?;
153+
let casm_class = CasmContractClass::from_contract_class(sierra_class, false, usize::MAX)?;
154+
155+
let hash_version = if use_blake2s { HashVersion::V2 } else { HashVersion::V1 };
156+
let hash = casm_class.hash(&hash_version);
157+
158+
Ok(Felt::from_bytes_be(&hash.0.to_bytes_be()))
159+
}

bin/sozo/src/commands/deploy.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use anyhow::{Context, Result};
2+
use clap::Args;
3+
use dojo_utils::{Deployer, TransactionResult, TxnConfig};
4+
use dojo_world::config::calldata_decoder::decode_calldata;
5+
use sozo_ui::SozoUi;
6+
use starknet::core::types::Felt;
7+
use starknet::core::utils::get_contract_address;
8+
use tracing::trace;
9+
10+
use super::options::account::AccountOptions;
11+
use super::options::starknet::StarknetOptions;
12+
use super::options::transaction::TransactionOptions;
13+
use crate::utils::{get_account_from_env, CALLDATA_DOC};
14+
15+
#[derive(Debug, Args)]
16+
#[command(about = "Deploy a declared class through the Universal Deployer Contract (UDC).")]
17+
pub struct DeployArgs {
18+
#[arg(value_name = "CLASS_HASH", help = "The class hash to deploy.")]
19+
pub class_hash: Felt,
20+
21+
#[arg(long, default_value = "0x0", help = "Salt to use for the deployment.")]
22+
pub salt: Felt,
23+
24+
#[arg(
25+
long,
26+
default_value = "0x0",
27+
help = "Deployer address to pass to the UDC. Defaults to zero for standard deployments."
28+
)]
29+
pub deployer_address: Felt,
30+
31+
#[arg(
32+
long = "constructor-calldata",
33+
value_name = "ARG",
34+
num_args = 0..,
35+
help = format!(
36+
"Constructor calldata elements (space separated).\n\n{}",
37+
CALLDATA_DOC
38+
)
39+
)]
40+
pub constructor_calldata: Vec<String>,
41+
42+
#[command(flatten)]
43+
pub transaction: TransactionOptions,
44+
45+
#[command(flatten)]
46+
pub starknet: StarknetOptions,
47+
48+
#[command(flatten)]
49+
pub account: AccountOptions,
50+
}
51+
52+
impl DeployArgs {
53+
pub async fn run(self, ui: &SozoUi) -> Result<()> {
54+
trace!(args = ?self);
55+
56+
let DeployArgs {
57+
class_hash,
58+
salt,
59+
deployer_address,
60+
constructor_calldata,
61+
transaction,
62+
starknet,
63+
account,
64+
} = self;
65+
66+
let constructor_felts = decode_calldata(&constructor_calldata)
67+
.context("Failed to parse constructor calldata")?;
68+
let expected_address =
69+
get_contract_address(salt, class_hash, &constructor_felts, deployer_address);
70+
71+
let txn_config: TxnConfig = transaction.try_into()?;
72+
73+
// TODO: allow custom headers from the CLI.
74+
let headers = None;
75+
let account = get_account_from_env(account, &starknet, headers).await?;
76+
77+
ui.title("Deploy contract");
78+
ui.step("Deploying contract via UDC");
79+
let params_ui = ui.subsection();
80+
params_ui.verbose(format!("Class hash : {:#066x}", class_hash));
81+
params_ui.verbose(format!("Salt : {salt:#066x}"));
82+
params_ui.verbose(format!("Deployer : {deployer_address:#066x}"));
83+
params_ui.verbose(format!("Expect addr: {expected_address:#066x}"));
84+
if constructor_felts.is_empty() {
85+
params_ui.verbose("Constructor calldata: <empty>");
86+
} else {
87+
params_ui.verbose(format!("Constructor felts ({})", constructor_felts.len()));
88+
for (idx, value) in constructor_felts.iter().enumerate() {
89+
params_ui.verbose(params_ui.indent(1, format!("[{idx}] {value:#066x}")));
90+
}
91+
}
92+
93+
let deployer = Deployer::new(account, txn_config);
94+
let (actual_address, tx_result) =
95+
deployer.deploy_via_udc(class_hash, salt, &constructor_felts, deployer_address).await?;
96+
97+
match tx_result {
98+
TransactionResult::Noop => {
99+
let address =
100+
if actual_address == Felt::ZERO { expected_address } else { actual_address };
101+
ui.result(format!("Contract already deployed.\n Address : {address:#066x}"));
102+
if address != expected_address {
103+
ui.warn(format!(
104+
"Computed address {expected_address:#066x} differs from on-chain \
105+
{address:#066x}."
106+
));
107+
}
108+
}
109+
TransactionResult::Hash(hash) => {
110+
let deployed =
111+
if actual_address == Felt::ZERO { expected_address } else { actual_address };
112+
ui.result(format!(
113+
"Deployment submitted.\n Tx hash : {hash:#066x}\n Address : \
114+
{deployed:#066x}"
115+
));
116+
if deployed != expected_address {
117+
ui.warn(format!(
118+
"Computed address {expected_address:#066x} differs from on-chain \
119+
{deployed:#066x}."
120+
));
121+
}
122+
}
123+
TransactionResult::HashReceipt(hash, receipt) => {
124+
let deployed =
125+
if actual_address == Felt::ZERO { expected_address } else { actual_address };
126+
ui.result(format!(
127+
"Deployment included on-chain.\n Tx hash : {hash:#066x}\n Address : \
128+
{deployed:#066x}"
129+
));
130+
if deployed != expected_address {
131+
ui.warn(format!(
132+
"Computed address {expected_address:#066x} differs from on-chain \
133+
{deployed:#066x}."
134+
));
135+
}
136+
ui.debug(format!("Receipt: {:?}", receipt));
137+
}
138+
}
139+
140+
Ok(())
141+
}
142+
}

bin/sozo/src/commands/mod.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ pub(crate) mod bindgen;
1111
pub(crate) mod build;
1212
pub(crate) mod call;
1313
pub(crate) mod clean;
14+
pub(crate) mod declare;
15+
pub(crate) mod deploy;
1416
pub(crate) mod events;
1517
pub(crate) mod execute;
1618
pub(crate) mod hash;
@@ -28,6 +30,8 @@ use bindgen::BindgenArgs;
2830
use build::BuildArgs;
2931
use call::CallArgs;
3032
use clean::CleanArgs;
33+
use declare::DeclareArgs;
34+
use deploy::DeployArgs;
3135
use events::EventsArgs;
3236
use execute::ExecuteArgs;
3337
use hash::HashArgs;
@@ -59,6 +63,10 @@ pub enum Commands {
5963
Clean(Box<CleanArgs>),
6064
#[command(about = "Computes hash with different hash functions")]
6165
Hash(Box<HashArgs>),
66+
#[command(about = "Declare Dojo contracts and Sozo-managed classes on Starknet")]
67+
Declare(Box<DeclareArgs>),
68+
#[command(about = "Deploy Dojo world or Sozo-managed contracts")]
69+
Deploy(Box<DeployArgs>),
6270
#[command(about = "Initialize a new dojo project")]
6371
Init(Box<InitArgs>),
6472
#[command(about = "Inspect the world")]
@@ -90,6 +98,8 @@ impl fmt::Display for Commands {
9098
Commands::Events(_) => write!(f, "Events"),
9199
Commands::Execute(_) => write!(f, "Execute"),
92100
Commands::Hash(_) => write!(f, "Hash"),
101+
Commands::Declare(_) => write!(f, "Declare"),
102+
Commands::Deploy(_) => write!(f, "Deploy"),
93103
Commands::Init(_) => write!(f, "Init"),
94104
Commands::Inspect(_) => write!(f, "Inspect"),
95105
Commands::Migrate(_) => write!(f, "Migrate"),
@@ -117,6 +127,8 @@ pub async fn run(command: Commands, scarb_metadata: &Metadata, ui: &SozoUi) -> R
117127
Commands::Events(args) => args.run(scarb_metadata, ui).await,
118128
Commands::Execute(args) => args.run(scarb_metadata, ui).await,
119129
Commands::Hash(args) => args.run(scarb_metadata),
130+
Commands::Declare(args) => args.run(ui).await,
131+
Commands::Deploy(args) => args.run(ui).await,
120132
Commands::Inspect(args) => args.run(scarb_metadata, ui).await,
121133
Commands::Mcp(args) => args.run(scarb_metadata).await,
122134
Commands::Migrate(args) => args.run(scarb_metadata, ui).await,

0 commit comments

Comments
 (0)