From ad1ce8b59aee7c26ba3d52c28054ad8715ed8618 Mon Sep 17 00:00:00 2001 From: "remy.baranx@gmail.com" Date: Sat, 11 Jan 2025 11:17:31 +0100 Subject: [PATCH 1/4] sozo: support multicall for execute command --- bin/sozo/src/commands/execute.rs | 209 ++++++++++++++------- crates/sozo/ops/src/resource_descriptor.rs | 2 +- 2 files changed, 140 insertions(+), 71 deletions(-) diff --git a/bin/sozo/src/commands/execute.rs b/bin/sozo/src/commands/execute.rs index fe5e37f62c..51c83d33a4 100644 --- a/bin/sozo/src/commands/execute.rs +++ b/bin/sozo/src/commands/execute.rs @@ -1,3 +1,5 @@ +use std::str::FromStr; + use anyhow::{anyhow, Result}; use clap::Args; use dojo_utils::{Invoker, TxnConfig}; @@ -9,6 +11,7 @@ use sozo_scarbext::WorkspaceExt; use sozo_walnut::WalnutDebugger; use starknet::core::types::Call; use starknet::core::utils as snutils; +use starknet_crypto::Felt; use tracing::trace; use super::options::account::AccountOptions; @@ -17,27 +20,64 @@ use super::options::transaction::TransactionOptions; use super::options::world::WorldOptions; use crate::utils; +#[derive(Debug, Clone)] +pub struct CallArguments { + pub tag_or_address: ResourceDescriptor, + pub entrypoint: String, + pub calldata: Vec, +} + +impl FromStr for CallArguments { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + let parts = s.splitn(3, ",").collect::>(); + + if parts.len() < 2 { + return Err(anyhow!( + "Expected call format: tag_or_address,entrypoint[,calldata1,...,calldataN]" + )); + } + + let tag_or_address = ResourceDescriptor::from_string(parts[0])?; + let entrypoint = parts[1].to_string(); + let calldata = + if parts.len() > 2 { calldata_decoder::decode_calldata(parts[2])? } else { vec![] }; + + Ok(CallArguments { tag_or_address, entrypoint, calldata }) + } +} + #[derive(Debug, Args)] -#[command(about = "Execute a system with the given calldata.")] +#[command(about = "Execute one or several systems with the given calldata.")] pub struct ExecuteArgs { - #[arg( - help = "The address or the tag (ex: dojo_examples:actions) of the contract to be executed." - )] - pub tag_or_address: ResourceDescriptor, + #[arg(num_args = 1..)] + #[arg(help = "A list of calls to execute.\n +A call is made up of 3 values, separated by a comma (,[,]): - #[arg(help = "The name of the entrypoint to be executed.")] - pub entrypoint: String, +- : the address or the tag (ex: dojo_examples-actions) of the contract to be \ + called, + +- : the name of the entry point to be called, + +- : the calldata to be passed to the system. + + Comma separated values e.g., 0x12345,128,u256:9999999999. + Sozo supports some prefixes that you can use to automatically parse some types. The supported \ + prefixes are: + - u256: A 256-bit unsigned integer. + - sstr: A cairo short string. + - str: A cairo string (ByteArray). + - int: A signed integer. + - no prefix: A cairo felt or any type that fit into one felt. - #[arg(short, long)] - #[arg(help = "The calldata to be passed to the system. Comma separated values e.g., \ - 0x12345,128,u256:9999999999. Sozo supports some prefixes that you can use to \ - automatically parse some types. The supported prefixes are: - - u256: A 256-bit unsigned integer. - - sstr: A cairo short string. - - str: A cairo string (ByteArray). - - int: A signed integer. - - no prefix: A cairo felt or any type that fit into one felt.")] - pub calldata: Option, +EXAMPLE + + sozo execute 0x1234,run ns-Actions,move,1,2 + +Executes the run function of the contract at the address 0x1234 without calldata, +and the move function of the ns-Actions contract, with the calldata [1,2].")] + pub calls: Vec, #[arg(long)] #[arg(help = "If true, sozo will compute the diff of the world from the chain to translate \ @@ -65,8 +105,6 @@ impl ExecuteArgs { let profile_config = ws.load_profile_config()?; - let descriptor = self.tag_or_address.ensure_namespace(&profile_config.namespace.default); - #[cfg(feature = "walnut")] let walnut_debugger = WalnutDebugger::new_from_flag( self.transaction.walnut, @@ -76,64 +114,62 @@ impl ExecuteArgs { let txn_config: TxnConfig = self.transaction.try_into()?; config.tokio_handle().block_on(async { - let (contract_address, contracts) = match &descriptor { - ResourceDescriptor::Address(address) => (Some(*address), Default::default()), - ResourceDescriptor::Tag(tag) => { - let contracts = utils::contracts_from_manifest_or_diff( - self.account.clone(), - self.starknet.clone(), - self.world, - &ws, - self.diff, - ) - .await?; - - (contracts.get(tag).map(|c| c.address), contracts) - } - ResourceDescriptor::Name(_) => { - unimplemented!("Expected to be a resolved tag with default namespace.") - } - }; - - let contract_address = contract_address.ok_or_else(|| { - let mut message = format!("Contract {descriptor} not found in the manifest."); - if self.diff { - message.push_str( - " Run the command again with `--diff` to force the fetch of data from the \ - chain.", - ); - } - anyhow!(message) - })?; - - trace!( - contract=?descriptor, - entrypoint=self.entrypoint, - calldata=?self.calldata, - "Executing Execute command." - ); - - let calldata = if let Some(cd) = self.calldata { - calldata_decoder::decode_calldata(&cd)? - } else { - vec![] - }; - - let call = Call { - calldata, - to: contract_address, - selector: snutils::get_selector_from_name(&self.entrypoint)?, - }; - let (provider, _) = self.starknet.provider(profile_config.env.as_ref())?; + let contracts = utils::contracts_from_manifest_or_diff( + self.account.clone(), + self.starknet.clone(), + self.world, + &ws, + self.diff, + ) + .await?; + let account = self .account .account(provider, profile_config.env.as_ref(), &self.starknet, &contracts) .await?; - let invoker = Invoker::new(&account, txn_config); - let tx_result = invoker.invoke(call).await?; + let mut invoker = Invoker::new(&account, txn_config); + + for call in self.calls { + let descriptor = + call.tag_or_address.ensure_namespace(&profile_config.namespace.default); + + let contract_address = match &descriptor { + ResourceDescriptor::Address(address) => Some(*address), + ResourceDescriptor::Tag(tag) => contracts.get(tag).map(|c| c.address), + ResourceDescriptor::Name(_) => { + unimplemented!("Expected to be a resolved tag with default namespace.") + } + }; + + let contract_address = contract_address.ok_or_else(|| { + let mut message = format!("Contract {descriptor} not found in the manifest."); + if self.diff { + message.push_str( + " Run the command again with `--diff` to force the fetch of data from \ + the chain.", + ); + } + anyhow!(message) + })?; + + trace!( + contract=?descriptor, + entrypoint=call.entrypoint, + calldata=?call.calldata, + "Executing Execute command." + ); + + invoker.add_call(Call { + to: contract_address, + selector: snutils::get_selector_from_name(&call.entrypoint)?, + calldata: call.calldata, + }); + } + + let tx_result = invoker.multicall().await?; #[cfg(feature = "walnut")] if let Some(walnut_debugger) = walnut_debugger { @@ -145,3 +181,36 @@ impl ExecuteArgs { }) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_call_arguments_from_str() { + let res = CallArguments::from_str("0x1234,run").unwrap(); + assert!(res.tag_or_address == ResourceDescriptor::from_string("0x1234").unwrap()); + assert!(res.entrypoint == "run"); + + let res = CallArguments::from_str("dojo-Player,run").unwrap(); + assert!(res.tag_or_address == ResourceDescriptor::from_string("dojo-Player").unwrap()); + assert!(res.entrypoint == "run"); + + let res = CallArguments::from_str("Player,run").unwrap(); + assert!(res.tag_or_address == ResourceDescriptor::from_string("Player").unwrap()); + assert!(res.entrypoint == "run"); + + let res = CallArguments::from_str("0x1234,run,1,2,3").unwrap(); + assert!(res.tag_or_address == ResourceDescriptor::from_string("0x1234").unwrap()); + assert!(res.entrypoint == "run"); + assert!(res.calldata == vec![Felt::ONE, Felt::TWO, Felt::THREE]); + + // missing entry point + let res = CallArguments::from_str("0x1234"); + assert!(res.is_err()); + + // bad tag_or_address format + let res = CallArguments::from_str("0x12X4,run"); + assert!(res.is_err()); + } +} diff --git a/crates/sozo/ops/src/resource_descriptor.rs b/crates/sozo/ops/src/resource_descriptor.rs index 99ad4a15cf..5401fab213 100644 --- a/crates/sozo/ops/src/resource_descriptor.rs +++ b/crates/sozo/ops/src/resource_descriptor.rs @@ -7,7 +7,7 @@ use anyhow::Result; use dojo_world::contracts::naming; use starknet::core::types::Felt; -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq)] pub enum ResourceDescriptor { Address(Felt), Name(String), From 09b00d1083ce00e5a2dbfba90eb817fdd1790de1 Mon Sep 17 00:00:00 2001 From: "remy.baranx@gmail.com" Date: Tue, 14 Jan 2025 15:47:59 +0100 Subject: [PATCH 2/4] use space instead of comma as separator --- bin/sozo/src/commands/execute.rs | 106 +++++------------- bin/sozo/src/commands/mod.rs | 2 +- .../dojo/world/src/config/calldata_decoder.rs | 4 +- 3 files changed, 32 insertions(+), 80 deletions(-) diff --git a/bin/sozo/src/commands/execute.rs b/bin/sozo/src/commands/execute.rs index 51c83d33a4..190e30cb26 100644 --- a/bin/sozo/src/commands/execute.rs +++ b/bin/sozo/src/commands/execute.rs @@ -1,5 +1,3 @@ -use std::str::FromStr; - use anyhow::{anyhow, Result}; use clap::Args; use dojo_utils::{Invoker, TxnConfig}; @@ -11,7 +9,6 @@ use sozo_scarbext::WorkspaceExt; use sozo_walnut::WalnutDebugger; use starknet::core::types::Call; use starknet::core::utils as snutils; -use starknet_crypto::Felt; use tracing::trace; use super::options::account::AccountOptions; @@ -20,40 +17,14 @@ use super::options::transaction::TransactionOptions; use super::options::world::WorldOptions; use crate::utils; -#[derive(Debug, Clone)] -pub struct CallArguments { - pub tag_or_address: ResourceDescriptor, - pub entrypoint: String, - pub calldata: Vec, -} - -impl FromStr for CallArguments { - type Err = anyhow::Error; - - fn from_str(s: &str) -> Result { - let parts = s.splitn(3, ",").collect::>(); - - if parts.len() < 2 { - return Err(anyhow!( - "Expected call format: tag_or_address,entrypoint[,calldata1,...,calldataN]" - )); - } - - let tag_or_address = ResourceDescriptor::from_string(parts[0])?; - let entrypoint = parts[1].to_string(); - let calldata = - if parts.len() > 2 { calldata_decoder::decode_calldata(parts[2])? } else { vec![] }; - - Ok(CallArguments { tag_or_address, entrypoint, calldata }) - } -} - #[derive(Debug, Args)] #[command(about = "Execute one or several systems with the given calldata.")] pub struct ExecuteArgs { #[arg(num_args = 1..)] - #[arg(help = "A list of calls to execute.\n -A call is made up of 3 values, separated by a comma (,[,]): + #[arg(required = true)] + #[arg(help = "A list of calls to execute, separated by a /. + +A call is made up of a , an and an optional : - : the address or the tag (ex: dojo_examples-actions) of the contract to be \ called, @@ -62,7 +33,7 @@ A call is made up of 3 values, separated by a comma (,: the calldata to be passed to the system. - Comma separated values e.g., 0x12345,128,u256:9999999999. + Space separated values e.g., 0x12345 128 u256:9999999999. Sozo supports some prefixes that you can use to automatically parse some types. The supported \ prefixes are: - u256: A 256-bit unsigned integer. @@ -73,11 +44,11 @@ A call is made up of 3 values, separated by a comma (,, + pub calls: Vec, #[arg(long)] #[arg(help = "If true, sozo will compute the diff of the world from the chain to translate \ @@ -132,9 +103,11 @@ impl ExecuteArgs { let mut invoker = Invoker::new(&account, txn_config); - for call in self.calls { - let descriptor = - call.tag_or_address.ensure_namespace(&profile_config.namespace.default); + let mut arg_iter = self.calls.into_iter(); + + while let Some(arg) = arg_iter.next() { + let tag_or_address = ResourceDescriptor::from_string(&arg)?; + let descriptor = tag_or_address.ensure_namespace(&profile_config.namespace.default); let contract_address = match &descriptor { ResourceDescriptor::Address(address) => Some(*address), @@ -155,17 +128,29 @@ impl ExecuteArgs { anyhow!(message) })?; + let entrypoint = + arg_iter.next().ok_or_else(|| anyhow!("Unexpected number of arguments"))?; + + let mut calldata = vec![]; + for arg in &mut arg_iter { + let arg = match arg.as_str() { + "/" | "-" | "\\" => break, + _ => calldata_decoder::decode_single_calldata(&arg)?, + }; + calldata.extend(arg); + } + trace!( contract=?descriptor, - entrypoint=call.entrypoint, - calldata=?call.calldata, - "Executing Execute command." + entrypoint=entrypoint, + calldata=?calldata, + "Decoded call." ); invoker.add_call(Call { to: contract_address, - selector: snutils::get_selector_from_name(&call.entrypoint)?, - calldata: call.calldata, + selector: snutils::get_selector_from_name(&entrypoint)?, + calldata, }); } @@ -181,36 +166,3 @@ impl ExecuteArgs { }) } } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_call_arguments_from_str() { - let res = CallArguments::from_str("0x1234,run").unwrap(); - assert!(res.tag_or_address == ResourceDescriptor::from_string("0x1234").unwrap()); - assert!(res.entrypoint == "run"); - - let res = CallArguments::from_str("dojo-Player,run").unwrap(); - assert!(res.tag_or_address == ResourceDescriptor::from_string("dojo-Player").unwrap()); - assert!(res.entrypoint == "run"); - - let res = CallArguments::from_str("Player,run").unwrap(); - assert!(res.tag_or_address == ResourceDescriptor::from_string("Player").unwrap()); - assert!(res.entrypoint == "run"); - - let res = CallArguments::from_str("0x1234,run,1,2,3").unwrap(); - assert!(res.tag_or_address == ResourceDescriptor::from_string("0x1234").unwrap()); - assert!(res.entrypoint == "run"); - assert!(res.calldata == vec![Felt::ONE, Felt::TWO, Felt::THREE]); - - // missing entry point - let res = CallArguments::from_str("0x1234"); - assert!(res.is_err()); - - // bad tag_or_address format - let res = CallArguments::from_str("0x12X4,run"); - assert!(res.is_err()); - } -} diff --git a/bin/sozo/src/commands/mod.rs b/bin/sozo/src/commands/mod.rs index 0170c6bc30..43a5b3f688 100644 --- a/bin/sozo/src/commands/mod.rs +++ b/bin/sozo/src/commands/mod.rs @@ -49,7 +49,7 @@ pub enum Commands { #[command(about = "Run a migration, declaring and deploying contracts as necessary to update \ the world")] Migrate(Box), - #[command(about = "Execute a system with the given calldata.")] + #[command(about = "Execute one or several systems with the given calldata.")] Execute(Box), #[command(about = "Inspect the world")] Inspect(Box), diff --git a/crates/dojo/world/src/config/calldata_decoder.rs b/crates/dojo/world/src/config/calldata_decoder.rs index fd74e0aa46..15d87112ad 100644 --- a/crates/dojo/world/src/config/calldata_decoder.rs +++ b/crates/dojo/world/src/config/calldata_decoder.rs @@ -140,7 +140,7 @@ pub fn decode_calldata(input: &str) -> DecoderResult> { let mut calldata = vec![]; for item in items { - calldata.extend(decode_inner(item)?); + calldata.extend(decode_single_calldata(item)?); } Ok(calldata) @@ -154,7 +154,7 @@ pub fn decode_calldata(input: &str) -> DecoderResult> { /// /// # Returns /// A vector of [`Felt`]s. -fn decode_inner(item: &str) -> DecoderResult> { +pub fn decode_single_calldata(item: &str) -> DecoderResult> { let item = item.trim(); let felts = if let Some((prefix, value)) = item.split_once(ITEM_PREFIX_DELIMITER) { From 01aad32679e16188bee3673b9e9d56fe6a7a095b Mon Sep 17 00:00:00 2001 From: "remy.baranx@gmail.com" Date: Tue, 14 Jan 2025 20:53:08 +0100 Subject: [PATCH 3/4] improve entrypoint error message --- bin/sozo/src/commands/execute.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/bin/sozo/src/commands/execute.rs b/bin/sozo/src/commands/execute.rs index 190e30cb26..75d46fb73b 100644 --- a/bin/sozo/src/commands/execute.rs +++ b/bin/sozo/src/commands/execute.rs @@ -106,8 +106,9 @@ impl ExecuteArgs { let mut arg_iter = self.calls.into_iter(); while let Some(arg) = arg_iter.next() { - let tag_or_address = ResourceDescriptor::from_string(&arg)?; - let descriptor = tag_or_address.ensure_namespace(&profile_config.namespace.default); + let tag_or_address = arg; + let descriptor = ResourceDescriptor::from_string(&tag_or_address)? + .ensure_namespace(&profile_config.namespace.default); let contract_address = match &descriptor { ResourceDescriptor::Address(address) => Some(*address), @@ -128,8 +129,12 @@ impl ExecuteArgs { anyhow!(message) })?; - let entrypoint = - arg_iter.next().ok_or_else(|| anyhow!("Unexpected number of arguments"))?; + let entrypoint = arg_iter.next().ok_or_else(|| { + anyhow!( + "You must specify the entry point of {tag_or_address} to call, and \ + optionally the calldata." + ) + })?; let mut calldata = vec![]; for arg in &mut arg_iter { From 6101109f5fe985f6b6907bd6ddb7b41ea11c8d66 Mon Sep 17 00:00:00 2001 From: glihm Date: Wed, 15 Jan 2025 21:25:43 -0600 Subject: [PATCH 4/4] fix: reword error message for entrypoint --- bin/sozo/src/commands/execute.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bin/sozo/src/commands/execute.rs b/bin/sozo/src/commands/execute.rs index 75d46fb73b..009e7cb820 100644 --- a/bin/sozo/src/commands/execute.rs +++ b/bin/sozo/src/commands/execute.rs @@ -131,8 +131,8 @@ impl ExecuteArgs { let entrypoint = arg_iter.next().ok_or_else(|| { anyhow!( - "You must specify the entry point of {tag_or_address} to call, and \ - optionally the calldata." + "You must specify the entry point of the contract `{tag_or_address}` to \ + invoke, and optionally the calldata." ) })?;