Skip to content

Commit

Permalink
feat(sozo): support multicall for execute command (#2897)
Browse files Browse the repository at this point in the history
* sozo: support multicall for execute command

* use space instead of comma as separator

* improve entrypoint error message

* fix: reword error message for entrypoint

---------

Co-authored-by: glihm <[email protected]>
  • Loading branch information
remybar and glihm authored Jan 16, 2025
1 parent e57d820 commit 4fb18a6
Show file tree
Hide file tree
Showing 4 changed files with 102 additions and 76 deletions.
170 changes: 98 additions & 72 deletions bin/sozo/src/commands/execute.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,26 +18,37 @@ use super::options::world::WorldOptions;
use crate::utils;

#[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(help = "The name of the entrypoint to be executed.")]
pub entrypoint: String,

#[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<String>,
#[arg(num_args = 1..)]
#[arg(required = true)]
#[arg(help = "A list of calls to execute, separated by a /.
A call is made up of a <TAG_OR_ADDRESS>, an <ENTRYPOINT> and an optional <CALLDATA>:
- <TAG_OR_ADDRESS>: the address or the tag (ex: dojo_examples-actions) of the contract to be \
called,
- <ENTRYPOINT>: the name of the entry point to be called,
- <CALLDATA>: the calldata to be passed to the system.
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.
- 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.
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<String>,

#[arg(long)]
#[arg(help = "If true, sozo will compute the diff of the world from the chain to translate \
Expand Down Expand Up @@ -65,8 +76,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,
Expand All @@ -76,64 +85,81 @@ 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);

let mut arg_iter = self.calls.into_iter();

while let Some(arg) = arg_iter.next() {
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),
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)
})?;

let entrypoint = arg_iter.next().ok_or_else(|| {
anyhow!(
"You must specify the entry point of the contract `{tag_or_address}` to \
invoke, and optionally the calldata."
)
})?;

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=entrypoint,
calldata=?calldata,
"Decoded call."
);

invoker.add_call(Call {
to: contract_address,
selector: snutils::get_selector_from_name(&entrypoint)?,
calldata,
});
}

let tx_result = invoker.multicall().await?;

#[cfg(feature = "walnut")]
if let Some(walnut_debugger) = walnut_debugger {
Expand Down
2 changes: 1 addition & 1 deletion bin/sozo/src/commands/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ pub enum Commands {
#[command(about = "Run a migration, declaring and deploying contracts as necessary to update \
the world")]
Migrate(Box<MigrateArgs>),
#[command(about = "Execute a system with the given calldata.")]
#[command(about = "Execute one or several systems with the given calldata.")]
Execute(Box<ExecuteArgs>),
#[command(about = "Inspect the world")]
Inspect(Box<InspectArgs>),
Expand Down
4 changes: 2 additions & 2 deletions crates/dojo/world/src/config/calldata_decoder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,7 @@ pub fn decode_calldata(input: &str) -> DecoderResult<Vec<Felt>> {
let mut calldata = vec![];

for item in items {
calldata.extend(decode_inner(item)?);
calldata.extend(decode_single_calldata(item)?);
}

Ok(calldata)
Expand All @@ -154,7 +154,7 @@ pub fn decode_calldata(input: &str) -> DecoderResult<Vec<Felt>> {
///
/// # Returns
/// A vector of [`Felt`]s.
fn decode_inner(item: &str) -> DecoderResult<Vec<Felt>> {
pub fn decode_single_calldata(item: &str) -> DecoderResult<Vec<Felt>> {
let item = item.trim();

let felts = if let Some((prefix, value)) = item.split_once(ITEM_PREFIX_DELIMITER) {
Expand Down
2 changes: 1 addition & 1 deletion crates/sozo/ops/src/resource_descriptor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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),
Expand Down

0 comments on commit 4fb18a6

Please sign in to comment.