From f2525128a9203c562808151e55a2cebdaa0e9379 Mon Sep 17 00:00:00 2001 From: Adam Spofford Date: Fri, 6 Mar 2026 03:38:38 -0800 Subject: [PATCH 1/3] Canister method picker --- crates/icp-cli/src/commands/canister/call.rs | 80 +++++++++++++++----- docs/reference/cli.md | 4 +- 2 files changed, 63 insertions(+), 21 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/call.rs b/crates/icp-cli/src/commands/canister/call.rs index 09a73025..b3743844 100644 --- a/crates/icp-cli/src/commands/canister/call.rs +++ b/crates/icp-cli/src/commands/canister/call.rs @@ -1,4 +1,5 @@ use anyhow::{Context as _, anyhow, bail}; +use candid::types::{Type, TypeInner}; use candid::{Encode, IDLArgs, Nat, Principal, TypeEnv, types::Function}; use candid_parser::assist; use candid_parser::parse_idl_args; @@ -37,8 +38,9 @@ pub(crate) struct CallArgs { #[command(flatten)] pub(crate) cmd_args: args::CanisterCommandArgs, - /// Name of canister method to call into - pub(crate) method: String, + /// Name of canister method to call into. + /// If not provided, an interactive prompt will be launched. + pub(crate) method: Option, /// Call arguments, interpreted per `--args-format` (Candid by default). /// If not provided, an interactive prompt will be launched. @@ -97,8 +99,29 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E ) .await?; - let candid_types = get_candid_type(&agent, cid, &args.method).await; + let candid_types = get_candid_type(&agent, cid).await; + let method = if let Some(method) = &args.method { + method.clone() + } else if let Some(interface) = &candid_types { + // Interactive method selection using candid assist + let methods: Vec<&str> = interface.methods().collect(); + if methods.is_empty() { + bail!("the canister's Candid interface has no methods"); + } + let selection = dialoguer::Select::new() + .with_prompt("Select a method to call") + .items(&methods) + .default(0) + .interact()?; + methods[selection].to_string() + } else { + bail!( + "method name was not provided and could not fetch candid type to assist method selection" + ); + }; + let declared_method = + candid_types.and_then(|i| Some((i.env.clone(), i.get_method(&method)?.clone()))); enum ResolvedArgs { Candid(IDLArgs), Bytes(Vec), @@ -141,7 +164,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E (Some(_), Some(_)) => unreachable!("clap conflicts_with prevents this"), }; - let arg_bytes = match (&candid_types, resolved_args) { + let arg_bytes = match (&declared_method, resolved_args) { (_, None) if args.args_format != InitArgsFormat::Candid => { bail!("arguments must be provided when --args-format is not candid"); } @@ -182,7 +205,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E // Route the call through the proxy canister let proxy_args = ProxyArgs { canister_id: cid, - method: args.method.clone(), + method: method.clone(), args: arg_bytes, cycles: Nat::from(args.cycles.get()), }; @@ -203,23 +226,22 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E } } else if args.query { // Preemptive check: error if Candid shows this is an update method - if let Some((_, func)) = &candid_types + if let Some((_, func)) = &declared_method && !func.is_query() { bail!( - "`{}` is an update method, not a query method. \ + "`{method}` is an update method, not a query method. \ Run the command without `--query`.", - args.method ); } agent - .query(&cid, &args.method) + .query(&cid, &method) .with_arg(arg_bytes) .call() .await? } else { // Direct update call to the target canister - agent.update(&cid, &args.method).with_arg(arg_bytes).await? + agent.update(&cid, &method).with_arg(arg_bytes).await? }; let mut term = Term::buffered_stdout(); @@ -227,7 +249,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E match args.output { CallOutputMode::Auto => { - if let Ok(ret) = try_decode_candid(&res, candid_types.as_ref()) { + if let Ok(ret) = try_decode_candid(&res, declared_method.as_ref()) { print_candid_for_term(&mut term, &ret) .context("failed to print candid return value")?; } else if let Ok(s) = std::str::from_utf8(&res) { @@ -239,7 +261,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E } } CallOutputMode::Candid => { - let ret = try_decode_candid(&res, candid_types.as_ref()).with_context(res_hex)?; + let ret = try_decode_candid(&res, declared_method.as_ref()).with_context(res_hex)?; print_candid_for_term(&mut term, &ret) .context("failed to print candid return value")?; } @@ -297,17 +319,37 @@ pub(crate) fn print_candid_for_term(term: &mut Term, args: &IDLArgs) -> io::Resu /// - the canister exposes its Candid interface in its metadata; /// - the IDL file can be parsed and type checked in Rust parser; /// - has an actor in the IDL file. If anything fails, it returns None. -async fn get_candid_type( - agent: &Agent, - canister_id: Principal, - method_name: &str, -) -> Option<(TypeEnv, Function)> { +async fn get_candid_type(agent: &Agent, canister_id: Principal) -> Option { let candid_interface = fetch_canister_metadata(agent, canister_id, "candid:service").await?; let candid_source = CandidSource::Text(&candid_interface); let (type_env, ty) = candid_source.load().ok()?; let actor = ty?; - let func = type_env.get_method(&actor, method_name).ok()?.clone(); - Some((type_env, func)) + Some(CanisterInterface { + env: type_env, + ty: actor, + }) +} + +struct CanisterInterface { + env: TypeEnv, + ty: Type, +} + +impl CanisterInterface { + fn methods(&self) -> impl Iterator { + let ty = if let TypeInner::Class(_, t) = &*self.ty.0 { + t + } else { + &self.ty + }; + let TypeInner::Service(methods) = &*ty.0 else { + unreachable!("check_prog should verify service type") + }; + methods.iter().map(|(name, _)| name.as_str()) + } + fn get_method<'a>(&'a self, method_name: &'a str) -> Option<&'a Function> { + self.env.get_method(&self.ty, method_name).ok() + } } #[cfg(test)] diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 49c7bf78..5ab7a519 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -141,12 +141,12 @@ Perform canister operations against a network Make a canister call -**Usage:** `icp canister call [OPTIONS] [ARGS]` +**Usage:** `icp canister call [OPTIONS] [METHOD] [ARGS]` ###### **Arguments:** * `` — Name or principal of canister to target. When using a name an environment must be specified -* `` — Name of canister method to call into +* `` — Name of canister method to call into. If not provided, an interactive prompt will be launched * `` — Call arguments, interpreted per `--args-format` (Candid by default). If not provided, an interactive prompt will be launched ###### **Options:** From c651139533142fb9c1ced94525f0501452dd467d Mon Sep 17 00:00:00 2001 From: Adam Spofford Date: Fri, 6 Mar 2026 07:55:36 -0800 Subject: [PATCH 2/3] typo --- crates/icp-cli/src/commands/canister/call.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/icp-cli/src/commands/canister/call.rs b/crates/icp-cli/src/commands/canister/call.rs index b3743844..1b6e8892 100644 --- a/crates/icp-cli/src/commands/canister/call.rs +++ b/crates/icp-cli/src/commands/canister/call.rs @@ -169,7 +169,7 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E bail!("arguments must be provided when --args-format is not candid"); } (None, None) => bail!( - "arguments was not provided and could not fetch candid type to assist building arguments" + "arguments were not provided and could not fetch candid type to assist building arguments" ), (None, Some(ResolvedArgs::Bytes(bytes))) => bytes, (None, Some(ResolvedArgs::Candid(arguments))) => { From 5de88a1dd7e03a6c59876745a477eaca35bc100f Mon Sep 17 00:00:00 2001 From: Adam Spofford Date: Fri, 6 Mar 2026 10:06:27 -0800 Subject: [PATCH 3/3] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4f47e14..20245f23 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,6 @@ # Unreleased +* feat: Leaving off the method name parameter in `icp canister call` prompts you with an interactive list of methods * fix: Correct templating of special HTML characters in recipes # v0.2.0