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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
# Unreleased

* feat: `icp canister snapshot` - create, delete, restore, list canister snapshots
* feat: `icp canister call` now supports `--proxy` flag to route calls through a proxy canister
* Use `--proxy <CANISTER_ID>` to forward the call through a proxy canister's `proxy` method
* Use `--cycles <AMOUNT>` to specify cycles to forward with the proxied call (defaults to 0)

# v0.1.0-beta.6

Expand Down
1 change: 1 addition & 0 deletions crates/icp-canister-interfaces/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,5 @@ pub mod governance;
pub mod icp_ledger;
pub mod internet_identity;
pub mod nns_root;
pub mod proxy;
pub mod registry;
72 changes: 72 additions & 0 deletions crates/icp-canister-interfaces/src/proxy.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
use candid::{CandidType, Nat, Principal};
use serde::Deserialize;

/// Arguments for the proxy canister's `proxy` method.
#[derive(Clone, Debug, CandidType, Deserialize)]
pub struct ProxyArgs {
/// The target canister to forward the call to.
pub canister_id: Principal,
/// The method name to invoke on the target canister.
pub method: String,
/// The serialized Candid arguments for the method.
pub args: Vec<u8>,
/// The number of cycles to forward with the call.
pub cycles: Nat,
}

/// Result from the proxy canister's `proxy` method.
#[derive(Clone, Debug, CandidType, Deserialize)]
pub enum ProxyResult {
/// The proxied call succeeded.
Ok(ProxyOk),
/// The proxied call failed.
Err(ProxyError),
}

/// Success result containing the response from the target canister.
#[derive(Clone, Debug, CandidType, Deserialize)]
pub struct ProxyOk {
/// The serialized Candid response from the target canister.
pub result: Vec<u8>,
}

/// Error variants from the proxy canister.
#[derive(Clone, Debug, CandidType, Deserialize)]
pub enum ProxyError {
/// The proxy canister does not have enough cycles to process the request.
InsufficientCycles {
/// The number of cycles available.
available: Nat,
/// The number of cycles required.
required: Nat,
},
/// The call to the target canister failed.
CallFailed {
/// A description of the failure reason.
reason: String,
},
/// The caller is not authorized to use this proxy canister.
UnauthorizedUser,
}

impl ProxyError {
/// Format the error for display.
pub fn format_error(&self) -> String {
match self {
ProxyError::InsufficientCycles {
available,
required,
} => {
format!(
"Proxy canister has insufficient cycles. Available: {available}, required: {required}"
)
}
ProxyError::CallFailed { reason } => {
format!("Proxy call failed: {reason}")
}
ProxyError::UnauthorizedUser => {
"Unauthorized: you are not in the proxy canister's controllers list".to_string()
}
}
}
}
46 changes: 44 additions & 2 deletions crates/icp-cli/src/commands/canister/call.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,19 @@
use anyhow::{Context as _, bail};
use candid::{IDLArgs, Principal, TypeEnv, types::Function};
use candid::{Encode, IDLArgs, Nat, Principal, TypeEnv, types::Function};
use candid_parser::assist;
use candid_parser::utils::CandidSource;
use clap::Args;
use dialoguer::console::Term;
use ic_agent::Agent;
use icp::context::Context;
use icp::prelude::*;
use icp_canister_interfaces::proxy::{ProxyArgs, ProxyResult};
use std::io::{self, Write};
use tracing::warn;

use crate::{
commands::args,
commands::parsers::parse_cycles_amount,
operations::misc::{ParsedArguments, fetch_canister_metadata, parse_args},
};

Expand All @@ -35,6 +37,20 @@ pub(crate) struct CallArgs {
///
/// If not provided, an interactive prompt will be launched to help build the arguments.
pub(crate) args: Option<String>,

/// Principal of a proxy canister to route the call through.
///
/// When specified, instead of calling the target canister directly,
/// the call will be sent to the proxy canister's `proxy` method,
/// which forwards it to the target canister.
#[arg(long)]
pub(crate) proxy: Option<Principal>,

/// Cycles to forward with the proxied call.
///
/// 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,
}

pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::Error> {
Expand Down Expand Up @@ -106,7 +122,33 @@ pub(crate) async fn exec(ctx: &Context, args: &CallArgs) -> Result<(), anyhow::E
.context("failed to serialize candid arguments with specific types")?,
};

let res = agent.update(&cid, &args.method).with_arg(arg_bytes).await?;
let res = if let Some(proxy_cid) = args.proxy {
// Route the call through the proxy canister
let proxy_args = ProxyArgs {
canister_id: cid,
method: args.method.clone(),
args: arg_bytes,
cycles: Nat::from(args.cycles),
};
let proxy_arg_bytes =
Encode!(&proxy_args).context("failed to encode proxy call arguments")?;

let proxy_res = agent
.update(&proxy_cid, "proxy")
.with_arg(proxy_arg_bytes)
.await?;

let proxy_result: (ProxyResult,) =
candid::decode_args(&proxy_res).context("failed to decode proxy canister response")?;

match proxy_result.0 {
ProxyResult::Ok(ok) => ok.result,
ProxyResult::Err(err) => bail!(err.format_error()),
}
} else {
// Direct call to the target canister
agent.update(&cid, &args.method).with_arg(arg_bytes).await?
};

let ret = IDLArgs::from_bytes(&res[..])?;

Expand Down
Binary file added crates/icp-cli/tests/assets/proxy.wasm
Binary file not shown.
101 changes: 101 additions & 0 deletions crates/icp-cli/tests/canister_call_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,104 @@ async fn canister_call_with_arguments_from_file() {
.success()
.stdout(eq("(\"Hello, world!\")").trim());
}

#[tokio::test]
async fn canister_call_through_proxy() {
let ctx = TestContext::new();

// Setup project
let project_dir = ctx.create_project_dir("icp");

// Use vendored WASMs
let target_wasm = ctx.make_asset("example_icp_mo.wasm");
let proxy_wasm = ctx.make_asset("proxy.wasm");

// Project manifest with both target and proxy canisters
let pm = formatdoc! {r#"
canisters:
- name: target
build:
steps:
- type: script
command: cp '{target_wasm}' "$ICP_WASM_OUTPUT_PATH"

- name: proxy
build:
steps:
- type: script
command: cp '{proxy_wasm}' "$ICP_WASM_OUTPUT_PATH"

{NETWORK_RANDOM_PORT}
{ENVIRONMENT_RANDOM_PORT}
"#};

write_string(&project_dir.join("icp.yaml"), &pm).expect("failed to write project manifest");

// Start network
let _g = ctx.start_network_in(&project_dir, "random-network").await;
ctx.ping_until_healthy(&project_dir, "random-network");

// Deploy both canisters
ctx.icp()
.current_dir(&project_dir)
.args(["deploy", "--environment", "random-environment"])
.assert()
.success();

// Get the proxy canister ID using canister status --id-only
let output = ctx
.icp()
.current_dir(&project_dir)
.args([
"canister",
"status",
"--environment",
"random-environment",
"--id-only",
"proxy",
])
.output()
.expect("failed to get proxy canister id");
let proxy_cid = String::from_utf8(output.stdout)
.expect("invalid utf8")
.trim()
.to_string();

// Test calling target canister through the proxy
ctx.icp()
.current_dir(&project_dir)
.args([
"canister",
"call",
"--environment",
"random-environment",
"target",
"greet",
"(\"proxy\")",
"--proxy",
&proxy_cid,
])
.assert()
.success()
.stdout(eq("(\"Hello, proxy!\")").trim());

// Test calling through proxy with cycles (should also work with 0 cycles)
ctx.icp()
.current_dir(&project_dir)
.args([
"canister",
"call",
"--environment",
"random-environment",
"target",
"greet",
"(\"world\")",
"--proxy",
&proxy_cid,
"--cycles",
"0",
])
.assert()
.success()
.stdout(eq("(\"Hello, world!\")").trim());
}
8 changes: 8 additions & 0 deletions docs/reference/cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,6 +152,14 @@ Make a canister call
* `-n`, `--network <NETWORK>` — Name of the network to target, conflicts with environment argument
* `-e`, `--environment <ENVIRONMENT>` — Override the environment to connect to. By default, the local environment is used
* `--identity <IDENTITY>` — The user identity to run this command as
* `--proxy <PROXY>` — Principal of a proxy canister to route the call through.

When specified, instead of calling the target canister directly, the call will be sent to the proxy canister's `proxy` method, which forwards it to the target canister.
* `--cycles <CYCLES>` — Cycles to forward with the proxied call.

Only used when --proxy is specified. Defaults to 0.

Default value: `0`



Expand Down
39 changes: 39 additions & 0 deletions examples/icp-proxy-canister/icp.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
# yaml-language-server: $schema=https://github.com/dfinity/icp-cli/raw/refs/tags/v0.1.0-beta.2/docs/schemas/icp-yaml-schema.json
#
# This example demonstrates using a proxy canister to forward calls to other canisters.
#
# The proxy canister implements the interface defined at:
# https://github.com/dfinity/proxy-canister
#
# Usage:
# # Deploy both canisters
# icp deploy
#
# # Get the proxy canister ID
# icp canister status --id-only proxy
#
# # Call the target canister directly
# icp canister call target greet '("World")'
#
# # Call through the proxy canister (replace with actual proxy canister ID)
# icp canister call target greet '("World")' --proxy <PROXY_CANISTER_ID>
#
# # Forward cycles with the proxied call
# icp canister call target greet '("World")' --proxy <PROXY_CANISTER_ID> --cycles 1t

canisters:
# A simple target canister to be called through the proxy
- name: target
build:
steps:
- type: pre-built
path: ../icp-pre-built/dist/hello_world.wasm
sha256: 17a05e36278cd04c7ae6d3d3226c136267b9df7525a0657521405e22ec96be7a

# The proxy canister from dfinity/proxy-canister
- name: proxy
build:
steps:
- type: pre-built
url: https://github.com/dfinity/proxy-canister/releases/download/v0.1.0-beta.1/proxy.wasm
sha256: 9e5abf27930b06df7f50c4baa1ec71b7e53408cb81849054fc51a1825eb6c5a4
Loading