Skip to content

Commit 54544bc

Browse files
authored
feat(sozo): add declare and deploy for standalone contracts (#3382)
* feat: add declare and deploy to sozo * fix: add headers + auto-blake detection * fix display of addresses and check priority of accounts * clean some args and display
1 parent 2bb9d20 commit 54544bc

File tree

10 files changed

+511
-3
lines changed

10 files changed

+511
-3
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: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
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+
let account = get_account_from_env(account, &starknet).await?;
52+
53+
let use_blake2s = if let Some(rpc_url) = starknet.rpc_url {
54+
if rpc_url.to_string().contains("sepolia") || rpc_url.to_string().contains("testnet") {
55+
true
56+
} else {
57+
starknet.use_blake2s_casm_class_hash
58+
}
59+
} else {
60+
starknet.use_blake2s_casm_class_hash
61+
};
62+
63+
let txn_config: TxnConfig = transaction.try_into()?;
64+
65+
ui.title("Declare contracts");
66+
67+
let mut prepared = Vec::new();
68+
for path in contracts {
69+
let class = prepare_class(&path, use_blake2s)
70+
.with_context(|| format!("Failed to prepare Sierra artifact {}", path.display()))?;
71+
72+
ui.step(format!("Compiled '{}' (class hash {:#066x})", class.label, class.class_hash));
73+
let detail_ui = ui.subsection();
74+
detail_ui.verbose(format!("CASM hash : {:#066x}", class.casm_class_hash));
75+
detail_ui.verbose(format!("Artifact : {}", path.display()));
76+
77+
prepared.push(class);
78+
}
79+
80+
let labeled = prepared
81+
.iter()
82+
.map(|class| LabeledClass {
83+
label: class.label.clone(),
84+
casm_class_hash: class.casm_class_hash,
85+
class: class.class.clone(),
86+
})
87+
.collect::<Vec<_>>();
88+
89+
let mut declarer = Declarer::new(account, txn_config);
90+
declarer.extend_classes(labeled);
91+
92+
let results = declarer.declare_all().await?;
93+
94+
let mut declared = 0usize;
95+
for (class, result) in prepared.iter().zip(results.iter()) {
96+
match result {
97+
TransactionResult::Noop => {
98+
ui.verbose(
99+
ui.indent(1, format!("'{}' already declared on-chain.", class.label)),
100+
);
101+
ui.verbose(ui.indent(2, format!("Class hash: {:#066x}", class.class_hash)));
102+
}
103+
TransactionResult::Hash(hash) => {
104+
declared += 1;
105+
ui.result(ui.indent(1, format!("'{}' declared.", class.label)));
106+
ui.verbose(ui.indent(2, format!("Class hash: {:#066x}", class.class_hash)));
107+
ui.verbose(ui.indent(2, format!("Tx hash : {hash:#066x}")));
108+
}
109+
TransactionResult::HashReceipt(hash, receipt) => {
110+
declared += 1;
111+
ui.result(ui.indent(1, format!("'{}' declared.", class.label)));
112+
ui.verbose(ui.indent(2, format!("Class hash: {:#066x}", class.class_hash)));
113+
ui.verbose(ui.indent(2, format!("Tx hash : {hash:#066x}")));
114+
ui.debug(format!("Receipt: {:?}", receipt));
115+
}
116+
}
117+
}
118+
119+
if declared == 0 {
120+
ui.result("All provided classes are already declared.");
121+
} else {
122+
ui.result(format!("Declared {} class(es).", declared));
123+
}
124+
125+
Ok(())
126+
}
127+
}
128+
129+
#[derive(Debug)]
130+
struct PreparedClass {
131+
label: String,
132+
class_hash: Felt,
133+
casm_class_hash: Felt,
134+
class: FlattenedSierraClass,
135+
}
136+
137+
fn prepare_class(path: &Path, use_blake2s: bool) -> Result<PreparedClass> {
138+
let data = fs::read(path)?;
139+
140+
let sierra: SierraClass = serde_json::from_slice(&data)?;
141+
let class_hash = sierra.class_hash()?;
142+
let flattened = sierra.clone().flatten()?;
143+
144+
let casm_hash = casm_class_hash_from_bytes(&data, use_blake2s)?;
145+
146+
let label = path
147+
.file_name()
148+
.and_then(|name| name.to_str())
149+
.ok_or_else(|| anyhow!("Unable to infer contract name from {}", path.display()))?
150+
.split('.')
151+
.next()
152+
.ok_or_else(|| anyhow!("Unable to infer contract name from {}", path.display()))?
153+
.to_string();
154+
155+
Ok(PreparedClass { label, class_hash, casm_class_hash: casm_hash, class: flattened })
156+
}
157+
158+
fn casm_class_hash_from_bytes(data: &[u8], use_blake2s: bool) -> Result<Felt> {
159+
let sierra_class: ContractClass = serde_json::from_slice(data)?;
160+
let casm_class = CasmContractClass::from_contract_class(sierra_class, false, usize::MAX)?;
161+
162+
let hash_version = if use_blake2s { HashVersion::V2 } else { HashVersion::V1 };
163+
let hash = casm_class.hash(&hash_version);
164+
165+
Ok(Felt::from_bytes_be(&hash.0.to_bytes_be()))
166+
}

bin/sozo/src/commands/deploy.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
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+
let account = get_account_from_env(account, &starknet).await?;
74+
75+
ui.title(format!("Deploy contract (class hash {:#066x})", class_hash));
76+
ui.step("Deploying contract via UDC");
77+
let params_ui = ui.subsection();
78+
params_ui.verbose(format!("Class hash : {:#066x}", class_hash));
79+
params_ui.verbose(format!("Salt : {salt:#066x}"));
80+
params_ui.verbose(format!("Deployer : {deployer_address:#066x}"));
81+
params_ui.verbose(format!("Expect addr: {expected_address:#066x}"));
82+
if constructor_felts.is_empty() {
83+
params_ui.verbose("Constructor calldata: <empty>");
84+
} else {
85+
params_ui.verbose(format!("Constructor felts ({})", constructor_felts.len()));
86+
for (idx, value) in constructor_felts.iter().enumerate() {
87+
params_ui.verbose(params_ui.indent(1, format!("[{idx}] {value:#066x}")));
88+
}
89+
}
90+
91+
let deployer = Deployer::new(account, txn_config);
92+
let (actual_address, tx_result) =
93+
deployer.deploy_via_udc(class_hash, salt, &constructor_felts, deployer_address).await?;
94+
95+
match tx_result {
96+
TransactionResult::Noop => {
97+
let address =
98+
if actual_address == Felt::ZERO { expected_address } else { actual_address };
99+
ui.result(format!("Contract already deployed.\n Address : {address:#066x}"));
100+
if address != expected_address {
101+
ui.warn(format!(
102+
"Computed address {expected_address:#066x} differs from on-chain \
103+
{address:#066x}."
104+
));
105+
}
106+
}
107+
TransactionResult::Hash(hash) => {
108+
let deployed =
109+
if actual_address == Felt::ZERO { expected_address } else { actual_address };
110+
ui.result(format!(
111+
"Deployment submitted.\n Tx hash : {hash:#066x}\n Address : \
112+
{deployed:#066x}"
113+
));
114+
if deployed != expected_address {
115+
ui.warn(format!(
116+
"Computed address {expected_address:#066x} differs from on-chain \
117+
{deployed:#066x}."
118+
));
119+
}
120+
}
121+
TransactionResult::HashReceipt(hash, receipt) => {
122+
let deployed =
123+
if actual_address == Felt::ZERO { expected_address } else { actual_address };
124+
ui.result(format!(
125+
"Contract deployed onchain.\n Tx hash : {hash:#066x}\n Address : \
126+
{deployed:#066x}"
127+
));
128+
if deployed != expected_address {
129+
ui.warn(format!(
130+
"Computed address {expected_address:#066x} differs from on-chain \
131+
{deployed:#066x}."
132+
));
133+
}
134+
ui.debug(format!("Receipt: {:?}", receipt));
135+
}
136+
}
137+
138+
Ok(())
139+
}
140+
}

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)