From b4343bcda6956ed5395d0e05bd423b1f94182bde Mon Sep 17 00:00:00 2001 From: Marco Walz Date: Thu, 12 Feb 2026 12:33:19 +0100 Subject: [PATCH] feat: add --query flag to canister call command --- crates/icp-cli/src/commands/canister/call.rs | 50 +++++++++++++++++++- crates/icp-cli/tests/canister_call_tests.rs | 41 +++++++++++++++- docs/reference/cli.md | 3 ++ 3 files changed, 92 insertions(+), 2 deletions(-) diff --git a/crates/icp-cli/src/commands/canister/call.rs b/crates/icp-cli/src/commands/canister/call.rs index 881a6ac8..9fe119ae 100644 --- a/crates/icp-cli/src/commands/canister/call.rs +++ b/crates/icp-cli/src/commands/canister/call.rs @@ -51,6 +51,13 @@ pub(crate) struct CallArgs { /// Only used when --proxy is specified. Defaults to 0. #[arg(long, requires = "proxy", value_parser = parse_cycles_amount, default_value = "0")] pub(crate) cycles: u128, + + /// Sends a query request to a canister instead of an update request. + /// + /// Query calls are faster but return uncertified responses. + /// Cannot be used with --proxy (proxy calls are always update calls). + #[arg(long, conflicts_with = "proxy")] + pub(crate) query: bool, } pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::Error> { @@ -145,8 +152,24 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E ProxyResult::Ok(ok) => ok.result, ProxyResult::Err(err) => bail!(err.format_error()), } + } else if args.query { + // Preemptive check: error if Candid shows this is an update method + if let Some((_, func)) = &candid_types + && !func.is_query() + { + bail!( + "`{}` is an update method, not a query method. \ + Run the command without `--query`.", + args.method + ); + } + agent + .query(&cid, &args.method) + .with_arg(arg_bytes) + .call() + .await? } else { - // Direct call to the target canister + // Direct update call to the target canister agent.update(&cid, &args.method).with_arg(arg_bytes).await? }; @@ -243,4 +266,29 @@ mod tests { "typed decoding should contain 'bitcoin_canister_id': {typed_str}" ); } + + #[test] + fn is_query_detects_method_types() { + let did = r#" + service : { + "get_value" : () -> (text) query; + "set_value" : (text) -> () + } + "#; + let source = CandidSource::Text(did); + let (type_env, ty) = source.load().unwrap(); + let actor = ty.unwrap(); + + let query_func = type_env.get_method(&actor, "get_value").unwrap(); + assert!( + query_func.is_query(), + "get_value should be detected as query" + ); + + let update_func = type_env.get_method(&actor, "set_value").unwrap(); + assert!( + !update_func.is_query(), + "set_value should be detected as update" + ); + } } diff --git a/crates/icp-cli/tests/canister_call_tests.rs b/crates/icp-cli/tests/canister_call_tests.rs index a410ca1d..033dcefc 100644 --- a/crates/icp-cli/tests/canister_call_tests.rs +++ b/crates/icp-cli/tests/canister_call_tests.rs @@ -1,5 +1,7 @@ use indoc::formatdoc; -use predicates::{ord::eq, str::PredicateStrExt}; +use predicates::ord::eq; +use predicates::prelude::PredicateBooleanExt; +use predicates::str::{PredicateStrExt, contains}; use crate::common::{ENVIRONMENT_RANDOM_PORT, NETWORK_RANDOM_PORT, TestContext}; use icp::fs::write_string; @@ -79,6 +81,23 @@ async fn canister_call_with_arguments() { .assert() .success() .stdout(eq("(\"Hello, world!\")").trim()); + + // Test calling with --query flag (greet is a query method in the Candid interface) + ctx.icp() + .current_dir(&project_dir) + .args([ + "canister", + "call", + "--environment", + "random-environment", + "--query", + "my-canister", + "greet", + "(\"world\")", + ]) + .assert() + .success() + .stdout(eq("(\"Hello, world!\")").trim()); } #[tokio::test] @@ -283,3 +302,23 @@ async fn canister_call_through_proxy() { .success() .stdout(eq("(\"Hello, world!\")").trim()); } + +#[tokio::test] +async fn canister_call_query_conflicts_with_proxy() { + let ctx = TestContext::new(); + + // --query and --proxy conflict at the clap level, so no network setup is needed. + ctx.icp() + .args([ + "canister", + "call", + "--query", + "--proxy", + "aaaaa-aa", + "some-canister", + "some-method", + ]) + .assert() + .failure() + .stderr(contains("--query").and(contains("--proxy"))); +} diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 2de6176e..e0eb5fbb 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -167,6 +167,9 @@ Make a canister call Only used when --proxy is specified. Defaults to 0. Default value: `0` +* `--query` — Sends a query request to a canister instead of an update request. + + Query calls are faster but return uncertified responses. Cannot be used with --proxy (proxy calls are always update calls).