Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
50 changes: 49 additions & 1 deletion crates/icp-cli/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Expand Down Expand Up @@ -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?
};

Expand Down Expand Up @@ -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"
);
}
}
41 changes: 40 additions & 1 deletion crates/icp-cli/tests/canister_call_tests.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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]
Expand Down Expand Up @@ -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")));
}
3 changes: 3 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -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).



Expand Down
Loading