use std::collections::BTreeMap;
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
use clap::Parser;
use miden_client::assembly::CodeBuilder;
use miden_client::keystore::Keystore;
use miden_client::transaction::{AdviceInputs, TransactionRequestBuilder, TransactionScript};
use miden_client::vm::{Package, PackageExport};
use miden_client::{Client, Deserializable, Felt, Word};
use crate::errors::CliError;
use crate::utils::{parse_account_id, print_executed_program_stack, print_executed_transaction};
#[derive(Debug, Clone, Parser)]
#[command(about = "Call a procedure on a local account and display the result and state delta")]
pub struct CallCmd {
#[arg(value_name = "ACCOUNT_ID:PROCEDURE")]
target: String,
#[arg(value_name = "args")]
args: Vec<String>,
#[arg(long, short)]
package: PathBuf,
}
impl CallCmd {
pub async fn execute<AUTH: Keystore + Sync + 'static>(
&self,
mut client: Client<AUTH>,
) -> Result<(), CliError> {
if client.get_sync_height().await? == 0.into() {
return Err(CliError::InvalidArgument(
"Client has not been synced yet. Run `miden-client sync` first.".to_string(),
));
}
let (account_str, procedure) = self.target.split_once(':').ok_or_else(|| {
CliError::InvalidArgument(format!(
"Expected `<ACCOUNT_ID>:<PROCEDURE>`, got '{}'.",
self.target
))
})?;
let account_id = parse_account_id(&client, account_str).await?;
client.try_get_account(account_id).await?;
let package = load_package(&self.package)?;
let digest = resolve_procedure_digest(&package, procedure)?;
let ProcedureSignature { param_count, result_count } =
print_manifest_signature(&package, procedure);
let args = parse_args(&self.args)?;
match param_count {
Some(expected) if args.len() != expected => {
return Err(CliError::InvalidArgument(format!(
"Procedure '{procedure}' expects {expected} argument(s), got {}.",
args.len()
)));
},
None => {
println!(
"Warning: no type info for procedure '{procedure}'. Skipping argument \
count check. Passing a wrong number of arguments may cause errors or \
wrong results."
);
},
_ => {},
}
let linked_builder =
client.code_builder().with_dynamically_linked_library(package.mast.as_ref())?;
let read_tx_script =
generate_tx_script(linked_builder.clone(), &digest, &args, result_count)?;
let output_stack = client
.execute_program(account_id, read_tx_script, AdviceInputs::default(), BTreeMap::new())
.await?;
print_executed_program_stack(&output_stack, result_count);
let delta_tx_script = generate_tx_script(linked_builder, &digest, &args, Some(0))?;
let tx_request = TransactionRequestBuilder::new()
.custom_script(delta_tx_script)
.build()
.map_err(|err| {
CliError::Transaction(err.into(), "Failed to build transaction".to_string())
})?;
match client.execute_transaction(account_id, tx_request).await {
Ok(tx_result) => {
print_executed_transaction(&mut client, tx_result.executed_transaction()).await?;
},
Err(e) => {
println!("\n(Could not compute state delta: {e})");
},
}
Ok(())
}
}
fn load_package(path: &Path) -> Result<Package, CliError> {
if !path.exists() {
return Err(CliError::InvalidArgument(format!(
"Package file not found: {}",
path.display()
)));
}
let bytes = std::fs::read(path)?;
Package::read_from_bytes(&bytes).map_err(|e| {
CliError::Parse(Box::new(e), format!("Failed to deserialize package: {}", path.display()))
})
}
fn resolve_procedure_digest(package: &Package, procedure_name: &str) -> Result<Word, CliError> {
let library = &*package.mast;
for module_info in library.module_infos() {
if let Some(digest) = module_info.get_procedure_digest_by_name(procedure_name) {
return Ok(digest);
}
}
let mut available = Vec::new();
for module_info in library.module_infos() {
for (_idx, proc_info) in module_info.procedures() {
available.push(format!(" {}::{}", module_info.path(), proc_info.name));
}
}
Err(CliError::InvalidArgument(format!(
"Procedure '{}' not found. Available:\n{}",
procedure_name,
available.join("\n")
)))
}
fn parse_args(args: &[String]) -> Result<Vec<Felt>, CliError> {
args.iter()
.map(|arg| {
let n = arg.parse::<u64>().map_err(|_| {
CliError::InvalidArgument(format!("Invalid argument '{arg}'. Expected u64."))
})?;
Felt::try_from(n)
.map_err(|_| CliError::InvalidArgument(format!("Argument '{arg}' is too large.")))
})
.collect()
}
struct ProcedureSignature {
param_count: Option<usize>,
result_count: Option<usize>,
}
fn print_manifest_signature(package: &Package, procedure_name: &str) -> ProcedureSignature {
const UNKNOWN: ProcedureSignature =
ProcedureSignature { param_count: None, result_count: None };
let kebab_name = procedure_name.replace('_', "-");
let quoted_kebab = format!("\"{kebab_name}\"");
let quoted_name = format!("\"{procedure_name}\"");
for export in package.manifest.exports() {
let PackageExport::Procedure(proc_export) = export else {
continue;
};
let path_str = proc_export.path.to_string();
if !path_str.ends_with(&kebab_name)
&& !path_str.ends_with(procedure_name)
&& !path_str.ends_with("ed_kebab)
&& !path_str.ends_with("ed_name)
{
continue;
}
if let Some(sig) = &proc_export.signature {
let params: Vec<String> = sig.params.iter().map(|p| format!("{p:?}")).collect();
let results: Vec<String> = sig.results.iter().map(|r| format!("{r:?}")).collect();
let ret_str = if results.is_empty() {
String::new()
} else {
format!(" -> ({})", results.join(", "))
};
let params_str = params.join(", ");
println!("Raw Signature: {procedure_name}({params_str}){ret_str}\n");
return ProcedureSignature {
param_count: Some(sig.params.len()),
result_count: Some(sig.results.len()),
};
}
println!("Raw Signature: {procedure_name}(...) [no type info]\n");
return UNKNOWN;
}
println!("(procedure '{procedure_name}' not found in manifest exports)");
println!("Available exports:");
for export in package.manifest.exports() {
if let PackageExport::Procedure(p) = export {
println!(" {}", p.path);
}
}
println!();
UNKNOWN
}
fn generate_tx_script(
code_builder: CodeBuilder,
digest: &Word,
args: &[Felt],
result_count: Option<usize>,
) -> Result<TransactionScript, CliError> {
if let Some(n) = result_count
&& n > 15
{
return Err(CliError::InvalidArgument(format!(
"Procedure returns {n} values; only up to 15 are supported."
)));
}
let mut script = String::from("begin\n");
for arg in args.iter().rev() {
writeln!(script, " push.{arg}").unwrap();
}
writeln!(script, " call.{}", digest.to_hex()).unwrap();
let to_drop = args.len();
if to_drop > 0 {
match result_count {
Some(0) => {
for _ in 0..to_drop {
script.push_str(" drop\n");
}
},
Some(1) => {
for _ in 0..to_drop {
script.push_str(" swap drop\n");
}
},
Some(n) => {
for _ in 0..to_drop {
writeln!(script, " movup.{n} drop").unwrap();
}
},
None => {},
}
}
script.push_str("end\n");
Ok(code_builder.compile_tx_script(&script)?)
}