Skip to content
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

Open
wants to merge 26 commits into
base: main
Choose a base branch
from
Open
Changes from 11 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
b384a89
Update execute.rs
Manush-2005 Nov 3, 2024
ae378aa
Few minor changes
Manush-2005 Nov 3, 2024
5751ee7
Keeping vec string
Manush-2005 Nov 4, 2024
8677707
Sperating each call using commas
Manush-2005 Nov 6, 2024
eebf651
Implementing Invoker before for loop and creating sep function for co…
Manush-2005 Nov 8, 2024
1ee5e0a
Using the Fromstr trait for string operations
Manush-2005 Nov 8, 2024
b3ed182
Checking local manifest for contract tag
Manush-2005 Nov 10, 2024
ce9e1b4
dummy commit
Manush-2005 Nov 10, 2024
ef4eee5
Dummy commit
Manush-2005 Nov 11, 2024
6e4a960
Merge conflict solved commit
Manush-2005 Nov 11, 2024
9a4eeb9
Getting contracts the right way and also letting string pass if there…
Manush-2005 Nov 12, 2024
0363f73
Update execute.rs
Manush-2005 Nov 3, 2024
1c9a191
Few minor changes
Manush-2005 Nov 3, 2024
7a10e6c
Keeping vec string
Manush-2005 Nov 4, 2024
58c5560
Sperating each call using commas
Manush-2005 Nov 6, 2024
9dbf346
Implementing Invoker before for loop and creating sep function for co…
Manush-2005 Nov 8, 2024
0bdfc44
Using the Fromstr trait for string operations
Manush-2005 Nov 8, 2024
2264ba1
Checking local manifest for contract tag
Manush-2005 Nov 10, 2024
b421952
dummy commit
Manush-2005 Nov 10, 2024
a3480ff
Dummy commit
Manush-2005 Nov 11, 2024
a7996a2
Merge conflict solved commit
Manush-2005 Nov 11, 2024
0f742d6
Getting contracts the right way and also letting string pass if there…
Manush-2005 Nov 12, 2024
46e4412
Merge branch 'Sozo-mutiple-execute' of https://github.com/Manush-2005…
Manush-2005 Nov 12, 2024
970e489
Problems of using self in the resolve_contract_address function
Manush-2005 Nov 12, 2024
e25f6c7
Fmt and also fixed some errors
Manush-2005 Nov 13, 2024
608bd9f
Some final changes
Manush-2005 Nov 13, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 113 additions & 60 deletions bin/sozo/src/commands/execute.rs
Original file line number Diff line number Diff line change
@@ -1,55 +1,106 @@
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();


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
Copy link

Choose a reason for hiding this comment

The 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 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.")
}
Comment on lines +105 to +107
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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
+        }

Committable suggestion skipped: line range outside the PR's diff.

}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

Fix contract address resolution implementation.

Ohayo, sensei! There are several issues in this function:

  1. The function uses self but is not a method (lines 90-92)
  2. The Name variant is unimplemented but should handle the default namespace case
 fn resolve_contract_address(
     descriptor: &ResourceDescriptor,
     world_diff: &WorldDiff,
+    options: &ExecuteArgs,
+    ws: &Workspace,
 ) -> Result<Address> {
     match descriptor {
         ResourceDescriptor::Address(address) => Ok(*address),
         ResourceDescriptor::Tag(tag) => {
             let contracts = utils::contracts_from_manifest_or_diff(
-                self.account.clone(),
-                self.starknet.clone(),
-                self.world,
+                options.account.clone(),
+                options.starknet.clone(),
+                options.world,
                 &ws,
-                self.diff,
+                options.diff,
             )
             .await?;

             (contracts.get(tag).map(|c| c.address), contracts)
         }
         ResourceDescriptor::Name(name) => {
-            unimplemented!("Expected to be a resolved tag with default namespace.")
+            // Handle name with default namespace
+            let tag = format!("{}:{}", profile_config.namespace.default, name);
+            resolve_contract_address(&ResourceDescriptor::Tag(tag), world_diff, options, ws)
         }
     }
 }

Committable suggestion skipped: line range outside the PR's diff.

}

impl ExecuteArgs {
Expand All @@ -60,19 +111,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()?;

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(),
Expand All @@ -82,43 +130,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(())
})
}
}
}