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
1,111 changes: 858 additions & 253 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"canhttp",
"examples/http_canister",
"ic-agent-canister-runtime",
"ic-canister-runtime",
"ic-mock-http-canister-runtime",
]
Expand All @@ -24,6 +25,7 @@ ciborium = "0.2.2"
futures-channel = "0.3.31"
futures-util = "0.3.31"
http = "1.3.1"
ic-agent = "0.44.3"
ic-canister-runtime = { path = "ic-canister-runtime" }
ic-cdk = "0.18.7"
ic-error-types = "0.2"
Expand Down
8 changes: 8 additions & 0 deletions ic-agent-canister-runtime/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Unreleased
23 changes: 23 additions & 0 deletions ic-agent-canister-runtime/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
[package]
name = "ic-agent-canister-runtime"
version = "0.1.0"
description = "Implementation of a canister runtime of the Internet Computer for `ic-agent`"
license.workspace = true
readme.workspace = true
homepage.workspace = true
authors.workspace = true
edition.workspace = true
include = ["src", "Cargo.toml", "CHANGELOG.md", "LICENSE", "README.md"]
repository.workspace = true
documentation = "https://docs.rs/ic-agent-canister-runtime"

[dependencies]
async-trait = { workspace = true }
candid = { workspace = true }
ic-agent = { workspace = true }
ic-canister-runtime = { workspace = true }
ic-error-types = { workspace = true }
serde = { workspace = true }

[dev-dependencies]
tokio = { workspace = true, features = ["full"] }
1 change: 1 addition & 0 deletions ic-agent-canister-runtime/LICENSE
1 change: 1 addition & 0 deletions ic-agent-canister-runtime/NOTICE
43 changes: 43 additions & 0 deletions ic-agent-canister-runtime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
[![Internet Computer portal](https://img.shields.io/badge/InternetComputer-grey?logo=internet%20computer&style=for-the-badge)](https://internetcomputer.org)
[![DFinity Forum](https://img.shields.io/badge/help-post%20on%20forum.dfinity.org-blue?style=for-the-badge)](https://forum.dfinity.org/)
[![GitHub license](https://img.shields.io/badge/license-Apache%202.0-blue.svg?logo=apache&style=for-the-badge)](LICENSE)


# `ic-agent-canister-runtime`

Library that implements the [`ic_canister_runtime`](https://crates.io/crates/ic-canister-runtime) crate's Runtime trait using [`ic-agent`](https://crates.io/crates/ic-agent).
This can be useful when, e.g., contacting a canister via ingress messages instead of via another canister.

## Usage

Add this to your `Cargo.toml` (see [crates.io](https://crates.io/crates/ic-agent-canister-runtime) for the latest version):

```toml
ic-agent-canister-runtime = "0.1.0"
ic-canister-runtime = "0.1.0"
```

Then, use the library to abstract your code making requests to canisters as follows:
```rust
use ic_agent_canister_runtime::AgentRuntime;
use ic_canister_runtime::Runtime;

let agent = ic_agent::agent::Agent::builder().build().unwrap();
let runtime = AgentRuntime::new(&agent);

// Make a request to the `http_request` example canister's `make_http_post_request` endpoint
// See: https://github.com/dfinity/canhttp/tree/main/examples/http_canister
let http_request_result: String = runtime
.update_call(canister_id, "make_http_post_request", (), 0)
.await
.expect("Call to `http_canister` failed");

assert!(http_request_result.contains("Hello, World!"));
assert!(http_request_result.contains("\"X-Id\": \"42\""));
```

See the [Rust documentation](https://docs.rs/ic-agent-canister-runtime) for more details.

## License

This project is licensed under the [Apache License 2.0](https://opensource.org/licenses/Apache-2.0).
125 changes: 125 additions & 0 deletions ic-agent-canister-runtime/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
//! Library that implements the [`ic_canister_runtime`](https://crates.io/crates/ic-canister-runtime)
//! crate's Runtime trait using [`ic-agent`](https://crates.io/crates/ic-agent).
//! This can be useful when, e.g., contacting a canister via ingress messages instead of via another
//! canister.

use async_trait::async_trait;
use candid::{decode_one, encode_args, utils::ArgumentEncoder, CandidType, Principal};
use ic_agent::{Agent, AgentError};
use ic_canister_runtime::{IcError, Runtime};
use ic_error_types::RejectCode;
use serde::de::DeserializeOwned;

/// Runtime for interacting with a canister through an [`ic_agent::Agent`].
/// This can be useful when, e.g., contacting a canister via ingress messages instead of via another
/// canister.
///
///
/// # Examples
///
/// Call the `make_http_post_request` endpoint on the example [`http_canister`].
/// ```rust, no_run
/// # #[allow(deref_nullptr)]
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// use ic_agent::agent::Agent;
/// use ic_agent_canister_runtime::AgentRuntime;
/// use ic_canister_runtime::Runtime;
/// # use candid::Principal;
///
/// let agent = Agent::builder().build().expect("Failed to initialize agent");
/// let runtime = AgentRuntime::new(&agent);
/// # let canister_id = Principal::anonymous();
/// let http_request_result: String = runtime
/// .update_call(canister_id, "make_http_post_request", (), 0)
/// .await
/// .expect("Call to `http_canister` failed");
///
/// assert!(http_request_result.contains("Hello, World!"));
/// assert!(http_request_result.contains("\"X-Id\": \"42\""));
/// # Ok(())
/// # }
/// ```
///
/// [`http_canister`]: https://github.com/dfinity/canhttp/tree/main/examples/http_canister/
#[derive(Clone, Debug)]
pub struct AgentRuntime<'a> {
agent: &'a Agent,
}

impl<'a> AgentRuntime<'a> {
/// Create a new [`AgentRuntime`] with the given [`Agent`].
pub fn new(agent: &'a Agent) -> Self {
Self { agent }
}
}

#[async_trait]
impl Runtime for AgentRuntime<'_> {
async fn update_call<In, Out>(
&self,
id: Principal,
method: &str,
args: In,
_cycles: u128,
) -> Result<Out, IcError>
where
In: ArgumentEncoder + Send,
Out: CandidType + DeserializeOwned,
{
self.agent
.update(&id, method)
.with_arg(encode_args(args).unwrap_or_else(panic_when_encode_fails))
.call_and_wait()
.await
.map_err(convert_agent_error)
.and_then(decode_agent_response)
}

async fn query_call<In, Out>(
&self,
id: Principal,
method: &str,
args: In,
) -> Result<Out, IcError>
where
In: ArgumentEncoder + Send,
Out: CandidType + DeserializeOwned,
{
self.agent
.query(&id, method)
.with_arg(encode_args(args).unwrap_or_else(panic_when_encode_fails))
.call()
.await
.map_err(convert_agent_error)
.and_then(decode_agent_response)
}
}

fn decode_agent_response<Out>(result: Vec<u8>) -> Result<Out, IcError>
where
Out: CandidType + DeserializeOwned,
{
decode_one::<Out>(&result).map_err(|e| IcError::CandidDecodeFailed {
message: e.to_string(),
})
}

fn convert_agent_error(e: AgentError) -> IcError {
if let AgentError::CertifiedReject { ref reject, .. } = e {
if let Ok(code) = RejectCode::try_from(reject.reject_code as u64) {
return IcError::CallRejected {
code,
message: reject.reject_message.clone(),
};
}
}
IcError::CallRejected {
code: RejectCode::SysFatal,
message: e.to_string(),
}
}

fn panic_when_encode_fails(err: candid::error::Error) -> Vec<u8> {
panic!("failed to encode args: {err}")
}
10 changes: 10 additions & 0 deletions release-plz.toml
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ name = "ic-canister-runtime"
#git_release_enable = false # enable GitHub releases
publish = true # enable `cargo publish`

[[package]]
name = "ic-agent-canister-runtime"
#git_release_enable = false # enable GitHub releases
publish = true # enable `cargo publish`

[[package]]
name = "ic-mock-http-canister-runtime"
#git_release_enable = false # enable GitHub releases
publish = true # enable `cargo publish`

[[package]]
name = "http_canister"
release = false # don't process this package