Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
41 changes: 41 additions & 0 deletions ic-agent-canister-runtime/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
[![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).
Comment thread
gregorydemay marked this conversation as resolved.
Outdated

## 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"
```

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).
110 changes: 110 additions & 0 deletions ic-agent-canister-runtime/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
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`].
///
///
/// # 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 std::ptr;
/// use ic_agent_canister_runtime::AgentRuntime;
/// use ic_canister_runtime::Runtime;
/// # use candid::Principal;
///
/// # let agent = unsafe { &*ptr::null() };
Comment thread
gregorydemay marked this conversation as resolved.
Outdated
/// 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 {
IcError::CallRejected {
code: RejectCode::SysFatal,
message: e.to_string(),
}
}
Comment thread
gregorydemay marked this conversation as resolved.

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