-
Notifications
You must be signed in to change notification settings - Fork 182
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat(sozo) : multicall from CLI #2679
base: main
Are you sure you want to change the base?
Changes from 10 commits
b384a89
ae378aa
5751ee7
8677707
eebf651
1ee5e0a
b3ed182
ce9e1b4
ef4eee5
6e4a960
9a4eeb9
0363f73
1c9a191
7a10e6c
58c5560
9dbf346
0bdfc44
2264ba1
b421952
a3480ff
a7996a2
0f742d6
46e4412
970e489
e25f6c7
608bd9f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -1,55 +1,102 @@ | ||||
use anyhow::{anyhow, Result}; | ||||
use clap::Args; | ||||
use std::str::FromStr; | ||||
use dojo_types::naming; | ||||
use dojo_utils::{Invoker, TxnConfig}; | ||||
use dojo_world::config::calldata_decoder; | ||||
use scarb::core::Config; | ||||
use sozo_ops::resource_descriptor::ResourceDescriptor; | ||||
use sozo_scarbext::WorkspaceExt; | ||||
use sozo_walnut::WalnutDebugger; | ||||
use starknet::core::types::Call; | ||||
use starknet::core::utils as snutils; | ||||
use tracing::trace; | ||||
|
||||
use super::options::account::AccountOptions; | ||||
use super::options::starknet::StarknetOptions; | ||||
use super::options::transaction::TransactionOptions; | ||||
use super::options::world::WorldOptions; | ||||
use crate::commands::calldata_decoder; | ||||
use crate::utils; | ||||
|
||||
#[derive(Debug, Args)] | ||||
#[command(about = "Execute a system with the given calldata.")] | ||||
pub struct ExecuteArgs { | ||||
#[arg( | ||||
help = "The address or the tag (ex: dojo_examples:actions) of the contract to be executed." | ||||
help = "List of calls to execute. Each call should be in format: <CONTRACT_ADDRESS/TAG>,<ENTRYPOINT>,<ARG1>,<ARG2>,... (ex: dojo_examples:actions,execute,1,2)" | ||||
)] | ||||
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>, | ||||
pub calls: Vec<String>, | ||||
|
||||
#[arg(long)] | ||||
#[arg(help = "If true, sozo will compute the diff of the world from the chain to translate \ | ||||
tags to addresses.")] | ||||
pub diff: bool, | ||||
|
||||
#[command(flatten)] | ||||
pub starknet: StarknetOptions, | ||||
pub starknet: StarknetOptions, | ||||
|
||||
#[command(flatten)] | ||||
pub account: AccountOptions, | ||||
|
||||
#[command(flatten)] | ||||
pub world: WorldOptions, | ||||
pub world: WorldOptions, | ||||
|
||||
#[command(flatten)] | ||||
pub transaction: TransactionOptions, | ||||
pub transaction: TransactionOptions, | ||||
} | ||||
|
||||
#[derive(Debug)] | ||||
pub struct CallArgs { | ||||
pub tag_or_address: ResourceDescriptor, // Contract address or tag | ||||
pub entrypoint: String, // Entrypoint to call | ||||
pub calldata: Option<String>, // Calldata to pass to the entrypoint | ||||
} | ||||
|
||||
|
||||
impl std::str::FromStr for CallArgs { | ||||
type Err = anyhow::Error; | ||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> { | ||||
let s = s.trim(); | ||||
if s.is_empty() { | ||||
return Err(anyhow!("Empty call string")); | ||||
} | ||||
|
||||
let parts: Vec<&str> = s.split(',').collect(); | ||||
if parts.len() < 2 { | ||||
return Err(anyhow!("Invalid call format. Expected format: <CONTRACT_NAME>,<ENTRYPOINT_NAME>,<ARG1>,<ARG2>,...")); | ||||
} | ||||
|
||||
let entrypoint = parts[1].trim(); | ||||
if entrypoint.is_empty() { | ||||
return Err(anyhow!("Empty entrypoint")); | ||||
} | ||||
if !entrypoint.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') { | ||||
return Err(anyhow!("Invalid entrypoint format. Must contain only alphanumeric characters and underscores")); | ||||
} | ||||
|
||||
Ok(CallArgs { | ||||
tag_or_address: parts[0].parse()?, | ||||
entrypoint: entrypoint.to_string(), | ||||
calldata: if parts.len() > 2 { Some(parts[2..].join(",")) } else { None }, | ||||
Comment on lines
+75
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Improve calldata parsing to handle commas within arguments. Ohayo, sensei! The current parsing logic joins calldata arguments with commas, which may cause issues if an argument itself contains a comma. Consider enhancing the parsing mechanism to handle such cases, possibly by using a different delimiter or supporting argument escaping. |
||||
}) | ||||
} | ||||
} | ||||
|
||||
fn resolve_contract_address( | ||||
descriptor: &ResourceDescriptor, | ||||
world_diff: &WorldDiff, | ||||
) -> Result<Address> { | ||||
match descriptor { | ||||
ResourceDescriptor::Address(address) => Ok(*address), | ||||
ResourceDescriptor::Tag(tag) => { | ||||
let selector = naming::compute_selector_from_tag(tag); | ||||
world_diff | ||||
.get_contract_address(selector) | ||||
.ok_or_else(|| anyhow!("Contract {descriptor} not found in the world diff.")) | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is not the way you can get the contract address, since the world diff is not always computed if we have a local manifest. dojo/bin/sozo/src/commands/execute.rs Line 81 in 81e9707
|
||||
} | ||||
ResourceDescriptor::Name(_) => { | ||||
unimplemented!("Expected to be a resolved tag with default namespace.") | ||||
} | ||||
Comment on lines
+105
to
+107
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Implement the Name variant resolution. Ohayo, sensei! The Name variant is currently unimplemented, which could cause runtime panics. - ResourceDescriptor::Name(_) => {
- unimplemented!("Expected to be a resolved tag with default namespace.")
- }
+ ResourceDescriptor::Name(name) => {
+ let tag = format!("{}:{}", profile_config.namespace.default, name);
+ resolve_contract_address(
+ &ResourceDescriptor::Tag(tag),
+ world_diff,
+ options,
+ ws
+ ).await
+ }
|
||||
} | ||||
} | ||||
|
||||
impl ExecuteArgs { | ||||
|
@@ -60,19 +107,16 @@ 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, | ||||
self.starknet.url(profile_config.env.as_ref())?, | ||||
); | ||||
|
||||
let txn_config: TxnConfig = self.transaction.into(); | ||||
let txn_config: TxnConfig = self.transaction.try_into()?; // Changed from `into()` to `try_into()` for better error handling | ||||
|
||||
config.tokio_handle().block_on(async { | ||||
// We could save the world diff computation extracting the account directly from the | ||||
// options. | ||||
// We could save the world diff computation extracting the account directly from the options. | ||||
let (world_diff, account, _) = utils::get_world_diff_and_account( | ||||
self.account, | ||||
self.starknet.clone(), | ||||
|
@@ -82,43 +126,48 @@ impl ExecuteArgs { | |||
) | ||||
.await?; | ||||
|
||||
let contract_address = match &descriptor { | ||||
ResourceDescriptor::Address(address) => Some(*address), | ||||
ResourceDescriptor::Tag(tag) => { | ||||
let selector = naming::compute_selector_from_tag(tag); | ||||
world_diff.get_contract_address(selector) | ||||
} | ||||
ResourceDescriptor::Name(_) => { | ||||
unimplemented!("Expected to be a resolved tag with default namespace.") | ||||
} | ||||
let mut invoker = Invoker::new(&account, txn_config); | ||||
|
||||
// Parse the Vec<String> into Vec<CallArgs> using FromStr | ||||
let call_args_list: Vec<CallArgs> = self.calls.iter() | ||||
.map(|s| s.parse()) | ||||
.collect::<Result<Vec<_>>>()?; | ||||
|
||||
for call_args in call_args_list { | ||||
let descriptor = call_args.tag_or_address.ensure_namespace(&profile_config.namespace.default); | ||||
|
||||
// Checking the contract tag in local manifest | ||||
let contract_address = if let Some(local_address) = ws.get_contract_address(&descriptor) { | ||||
local_address | ||||
} else { | ||||
resolve_contract_address(&descriptor, &world_diff)? | ||||
}; | ||||
|
||||
trace!( | ||||
contract=?descriptor, | ||||
entrypoint=call_args.entrypoint, | ||||
calldata=?call_args.calldata, | ||||
"Executing Execute command." | ||||
); | ||||
|
||||
let calldata = if let Some(cd) = call_args.calldata { | ||||
calldata_decoder::decode_calldata(&cd)? | ||||
} else { | ||||
vec![] | ||||
}; | ||||
|
||||
let call = Call { | ||||
calldata, | ||||
to: contract_address, | ||||
selector: snutils::get_selector_from_name(&call_args.entrypoint)?, | ||||
}; | ||||
|
||||
invoker.add_call(call); // Adding each call to the Invoker | ||||
} | ||||
.ok_or_else(|| anyhow!("Contract {descriptor} not found in the world diff."))?; | ||||
|
||||
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 invoker = Invoker::new(&account, txn_config); | ||||
// TODO: add walnut back, perhaps at the invoker level. | ||||
let tx_result = invoker.invoke(call).await?; | ||||
|
||||
let tx_result = invoker.invoke().await?; // Invoking the multi-call | ||||
println!("{}", tx_result); | ||||
Ok(()) | ||||
}) | ||||
} | ||||
} | ||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The call args may be empty, some entrypoints don't take any arguments.