Skip to content

Commit 19b5f33

Browse files
committed
Create canruntime crate
1 parent 8a05d9c commit 19b5f33

File tree

7 files changed

+252
-1
lines changed

7 files changed

+252
-1
lines changed

Cargo.lock

Lines changed: 23 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[workspace]
2-
members = ["canhttp", "examples/http_canister"]
2+
members = ["canhttp", "canruntime", "examples/http_canister"]
33
resolver = "2"
44

55
[workspace.package]
@@ -12,6 +12,7 @@ readme = "README.md"
1212

1313
[workspace.dependencies]
1414
assert_matches = "1.5.0"
15+
async-trait = "0.1.88"
1516
candid = { version = "0.10.13" }
1617
ciborium = "0.2.2"
1718
futures-channel = "0.3.31"

canruntime/CHANGELOG.md

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
# Changelog
2+
3+
All notable changes to this project will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.3.0] - 2025-10-08
9+
10+
### Added
11+
- **Breaking:** A new method `charge_cycles` that does the actual charging was added to `CyclesChargingPolicy` ([#7](https://github.com/dfinity/canhttp/pull/7))
12+
- Example of canister using the library to make HTTP requests ([#6](https://github.com/dfinity/canhttp/pull/6))
13+
14+
### Changed
15+
- **Breaking:** Update `ic-cdk` to `v0.18.7` including several changes to align with the new HTTP outcall API ([#21](https://github.com/dfinity/canhttp/pull/21)). Notably:
16+
- `IcError` is refactored into an enum
17+
- Use of the new `HttpRequestArgs` and `HttpRequestResult` types in `CyclesChargingPolicy` and `Client` trait impls
18+
- Removal of `IcHttpRequestWithCycles`, `CyclesCostEstimator`, `CyclesAccountingError` and `CyclesAccounting` due to the `ic-cdk` method for making HTTP outcalls now taking care of charging cycles
19+
20+
[0.3.0]: https://github.com/dfinity/canhttp/compare/canhttp-v0.2.1..canhttp-v0.3.0
21+
22+
## [0.2.1] - 2025-07-11
23+
24+
### Added
25+
26+
- An `iter` method to `canhttp::multi::MultiResults` returning a borrowing iterator.
27+
28+
### Changed
29+
- The `canhttp` crate has been moved from the [`evm-rpc-canister`](https://github.com/dfinity/evm-rpc-canister) repository to the new [`canhttp`](https://github.com/dfinity/canhttp) repository.
30+
31+
[0.2.1]: https://github.com/dfinity/canhttp/compare/canhttp-v0.2.0..canhttp-v0.2.1
32+
33+
## [0.2.0] - 2025-07-08
34+
35+
### Added
36+
- Data structures `TimedSizedVec<T>` and `TimedSizedMap<K, V>` to store a limited number of expiring elements ([#434](https://github.com/dfinity/evm-rpc-canister/pull/434))
37+
- Method to list `Ok` results in a `MultiResults` ([#435](https://github.com/dfinity/evm-rpc-canister/pull/435))
38+
39+
### Changed
40+
41+
- **Breaking:** change the `code` field in the `IcError` type to use `ic_error_types::RejectCode` instead of `ic_cdk::api::call::RejectionCode` ([#428](https://github.com/dfinity/evm-rpc-canister/pull/428))
42+
43+
[0.2.0]: https://github.com/dfinity/canhttp/compare/canhttp-v0.1.0..canhttp-v0.2.0
44+
45+
## [0.1.0] - 2025-06-04
46+
47+
### Added
48+
49+
- JSON-RPC request ID with constant binary size ([#397](https://github.com/dfinity/evm-rpc-canister/pull/397))
50+
- Use `canhttp` to make parallel calls ([#391](https://github.com/dfinity/evm-rpc-canister/pull/391))
51+
- Improve validation of JSON-RPC requests and responses to adhere to the JSON-RPC specification ([#386](https://github.com/dfinity/evm-rpc-canister/pull/386) and [#387](https://github.com/dfinity/evm-rpc-canister/pull/387))
52+
- Retry layer ([#378](https://github.com/dfinity/evm-rpc-canister/pull/378))
53+
- JSON RPC conversion layer ([#375](https://github.com/dfinity/evm-rpc-canister/pull/375))
54+
- HTTP conversion layer ([#374](https://github.com/dfinity/evm-rpc-canister/pull/374))
55+
- Observability layer ([#370](https://github.com/dfinity/evm-rpc-canister/pull/370))
56+
- Library `canhttp` ([#364](https://github.com/dfinity/evm-rpc-canister/pull/364))
57+
58+
[0.1.0]: https://github.com/dfinity/canhttp/releases/tag/canhttp-v0.1.0

canruntime/Cargo.toml

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
[package]
2+
name = "canruntime"
3+
version = "0.1.0"
4+
description = "Rust library abstracts the canister runtime on the Internet Computer"
5+
license.workspace = true
6+
readme.workspace = true
7+
homepage.workspace = true
8+
authors.workspace = true
9+
edition.workspace = true
10+
include = ["src", "Cargo.toml", "CHANGELOG.md", "LICENSE", "README.md"]
11+
repository.workspace = true
12+
documentation = "https://docs.rs/canruntime"
13+
14+
[dependencies]
15+
async-trait = { workspace = true }
16+
candid = { workspace = true }
17+
ic-cdk = { workspace = true }
18+
ic-error-types = { workspace = true }
19+
serde = { workspace = true }
20+
thiserror = { workspace = true }
21+
22+
[dev-dependencies]

canruntime/LICENSE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../LICENSE

canruntime/NOTICE

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../NOTICE

canruntime/src/lib.rs

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
use async_trait::async_trait;
2+
use candid::{utils::ArgumentEncoder, CandidType, Principal};
3+
use ic_cdk::call::{Call, CallFailed, CandidDecodeFailed};
4+
use ic_error_types::RejectCode;
5+
use serde::de::DeserializeOwned;
6+
use thiserror::Error;
7+
8+
/// Abstract the canister runtime so that code making requests to canisters can be reused:
9+
/// * in production using [`ic_cdk`],
10+
/// * in unit tests by mocking this trait,
11+
/// * in integration tests by implementing this trait for `PocketIc`.
12+
#[async_trait]
13+
pub trait Runtime {
14+
/// Defines how asynchronous inter-canister update calls are made.
15+
async fn update_call<In, Out>(
16+
&self,
17+
id: Principal,
18+
method: &str,
19+
args: In,
20+
cycles: u128,
21+
) -> Result<Out, IcError>
22+
where
23+
In: ArgumentEncoder + Send,
24+
Out: CandidType + DeserializeOwned;
25+
26+
/// Defines how asynchronous inter-canister query calls are made.
27+
async fn query_call<In, Out>(
28+
&self,
29+
id: Principal,
30+
method: &str,
31+
args: In,
32+
) -> Result<Out, IcError>
33+
where
34+
In: ArgumentEncoder + Send,
35+
Out: CandidType + DeserializeOwned;
36+
}
37+
38+
/// Runtime when interacting with a canister running on the Internet Computer.
39+
#[derive(Copy, Clone, Eq, PartialEq, Debug)]
40+
pub struct IcRuntime;
41+
42+
#[async_trait]
43+
impl Runtime for IcRuntime {
44+
async fn update_call<In, Out>(
45+
&self,
46+
id: Principal,
47+
method: &str,
48+
args: In,
49+
cycles: u128,
50+
) -> Result<Out, IcError>
51+
where
52+
In: ArgumentEncoder + Send,
53+
Out: CandidType + DeserializeOwned,
54+
{
55+
Call::unbounded_wait(id, method)
56+
.with_args(&args)
57+
.with_cycles(cycles)
58+
.await
59+
.map_err(IcError::from)
60+
.and_then(|response| response.candid::<Out>().map_err(IcError::from))
61+
}
62+
63+
async fn query_call<In, Out>(
64+
&self,
65+
id: Principal,
66+
method: &str,
67+
args: In,
68+
) -> Result<Out, IcError>
69+
where
70+
In: ArgumentEncoder + Send,
71+
Out: CandidType + DeserializeOwned,
72+
{
73+
Call::unbounded_wait(id, method)
74+
.with_args(&args)
75+
.await
76+
.map_err(IcError::from)
77+
.and_then(|response| response.candid::<Out>().map_err(IcError::from))
78+
}
79+
}
80+
81+
/// Error returned by the Internet Computer when making an inter-canister call.
82+
#[derive(Error, Clone, Debug, PartialEq, Eq)]
83+
pub enum IcError {
84+
/// The liquid cycle balance is insufficient to perform the call.
85+
#[error("Insufficient liquid cycles balance, available: {available}, required: {required}")]
86+
InsufficientLiquidCycleBalance {
87+
/// The liquid cycle balance available in the canister.
88+
available: u128,
89+
/// The required cycles to perform the call.
90+
required: u128,
91+
},
92+
93+
/// The `ic0.call_perform` operation failed when performing the inter-canister call.
94+
#[error("Inter-canister call perform failed")]
95+
CallPerformFailed,
96+
97+
/// The inter-canister call is rejected.
98+
#[error("Inter-canister call rejected: {code:?} - {message})")]
99+
CallRejected {
100+
/// Rejection code as specified [here](https://internetcomputer.org/docs/current/references/ic-interface-spec#reject-codes)
101+
code: RejectCode,
102+
/// Associated helper message.
103+
message: String,
104+
},
105+
106+
/// The response from the inter-canister call could not be decoded as Candid.
107+
#[error("The inter-canister call response could not be decoded: {message}")]
108+
CandidDecodeFailed {
109+
/// The specific Candid error that occurred.
110+
message: String,
111+
},
112+
}
113+
114+
impl From<CallFailed> for IcError {
115+
fn from(err: CallFailed) -> Self {
116+
match err {
117+
CallFailed::CallPerformFailed(_) => IcError::CallPerformFailed,
118+
CallFailed::CallRejected(e) => {
119+
IcError::CallRejected {
120+
// `CallRejected::reject_code()` can only return an error result if there is a
121+
// new error code on ICP that the CDK is not aware of. We map it to `SysFatal`
122+
// since none of the other error codes apply.
123+
// In particular, note that `RejectCode::SysUnknown` is only applicable to
124+
// inter-canister calls that used `ic0.call_with_best_effort_response`.
125+
code: e.reject_code().unwrap_or(RejectCode::SysFatal),
126+
message: e.reject_message().to_string(),
127+
}
128+
}
129+
CallFailed::InsufficientLiquidCycleBalance(e) => {
130+
IcError::InsufficientLiquidCycleBalance {
131+
available: e.available,
132+
required: e.required,
133+
}
134+
}
135+
}
136+
}
137+
}
138+
139+
impl From<CandidDecodeFailed> for IcError {
140+
fn from(err: CandidDecodeFailed) -> Self {
141+
IcError::CandidDecodeFailed {
142+
message: err.to_string(),
143+
}
144+
}
145+
}

0 commit comments

Comments
 (0)