Skip to content
Closed
Show file tree
Hide file tree
Changes from 7 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
5 changes: 5 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ members = [
"ic-task-scheduler",
"ic-task-scheduler/tests/dummy_scheduler_canister",
"ic-test-utils",
"ic-http-outcall/api",
"ic-http-outcall/proxy-canister",
"ic-http-outcall/proxy-client",
]

[workspace.package]
Expand All @@ -40,6 +43,7 @@ async-trait = "0.1"
auto_ops = "0.3"
bincode = "1.3"
cfg-if = "1.0"
clap = "4.5"
criterion = "0.5.1"
crypto-bigint = { version = "0.5", features = ["serde"] }
dirs = "5.0"
Expand Down Expand Up @@ -73,6 +77,7 @@ tempfile = "3.14"
thiserror = "2.0"
tokio = "1.41"
trybuild = "1.0"
env_logger = "0.11"

# IC dependencies
candid = "0.10"
Expand Down
17 changes: 16 additions & 1 deletion ic-helpers/src/principal/management.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,11 @@ use ic_exports::ic_cdk::api::management_canister::ecdsa::{
EcdsaCurve, EcdsaKeyId, EcdsaPublicKeyArgument, EcdsaPublicKeyResponse, SignWithEcdsaArgument,
SignWithEcdsaResponse,
};
use ic_exports::ic_cdk::api::management_canister::http_request::{
CanisterHttpRequestArgument, HttpResponse,
};
use ic_exports::ic_cdk::api::management_canister::provisional::CanisterId;
use ic_exports::ic_kit::ic;
use ic_exports::ic_kit::{ic, CallResult};
use serde::{Deserialize, Serialize};

use super::private::Sealed;
Expand Down Expand Up @@ -143,6 +146,8 @@ pub trait ManagementPrincipalExt: Sealed {
async fn deposit_cycles(&self) -> Result<(), (RejectionCode, String)>;
async fn raw_rand(&self) -> Result<Vec<u8>, (RejectionCode, String)>;
async fn provisional_top_up(&self, amount: Nat) -> Result<(), (RejectionCode, String)>;

async fn http_request(&self, args: CanisterHttpRequestArgument) -> CallResult<HttpResponse>;
}

#[async_trait]
Expand Down Expand Up @@ -345,6 +350,16 @@ impl ManagementPrincipalExt for Principal {
)
.await
}

async fn http_request(&self, args: CanisterHttpRequestArgument) -> CallResult<HttpResponse> {
virtual_canister_call!(
Principal::management_canister(),
"http_request",
(args,),
HttpResponse
)
.await
}
}

#[derive(CandidType, Serialize, Deserialize, Debug)]
Expand Down
17 changes: 17 additions & 0 deletions ic-http-outcall/api/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "ic-http-outcall-api"
version.workspace = true
edition.workspace = true

[dependencies]
ic-exports = { path = "../../ic-exports" }
ic-helpers = { path = "../../ic-helpers", features = ["management_canister"] }
ic-canister = { path = "../../ic-canister/ic-canister", optional = true }

candid = { workspace = true }
serde = { workspace = true }
futures = { workspace = true }

[features]
proxy-api = []
non-replicated = ["proxy-api", "ic-canister"]
25 changes: 25 additions & 0 deletions ic-http-outcall/api/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
//! This crate provides an abstraction over different implementations of IC
//! Http outcall mechanism.
//!
//! The absraction is described as `HttpOutcall` trait. It has the
//! following implementations:
//! - `NonReplicatedHttpOutcall` - performs non-replicated call.
//! Details: `https://forum.dfinity.org/t/non-replicated-https-outcalls/26627`;
//!
//! - `ReplicatedHttpOutcall` - perform replicated calls using basic `http_outcall`
//! method in IC API.

#[cfg(feature = "non-replicated")]
mod non_replicated;
#[cfg(feature = "proxy-api")]
mod proxy_types;

mod outcall;
mod replicated;

#[cfg(feature = "non-replicated")]
pub use non_replicated::NonReplicatedHttpOutcall;
pub use outcall::HttpOutcall;
#[cfg(feature = "proxy-api")]
pub use proxy_types::{InitArgs, RequestArgs, RequestId, ResponseResult, REQUEST_METHOD_NAME};
pub use replicated::ReplicatedHttpOutcall;
123 changes: 123 additions & 0 deletions ic-http-outcall/api/src/non_replicated.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use std::time::Duration;

use candid::Principal;
use futures::channel::oneshot;
use ic_canister::virtual_canister_call;
use ic_exports::ic_cdk::api::management_canister::http_request::{
CanisterHttpRequestArgument, HttpResponse,
};
use ic_exports::ic_kit::{ic, CallResult, RejectionCode};

use crate::outcall::HttpOutcall;
use crate::proxy_types::{RequestArgs, RequestId, REQUEST_METHOD_NAME};
use crate::ResponseResult;

/// Callback type, which should be called to make the `HttpOutcall::request` function return.
pub type OnResponse = Box<dyn Fn(Vec<ResponseResult>)>;

/// Non-replicated http outcall implementation, which works together with ProxyCanister and ProxyCanisterClient.
///
/// # Workflow
/// 1. Client code calls `HttpOutcall::request(params)`. `Self` sends request params to ProxyCanister
/// and awaits a Waker, which will be awaken, once the ProxyCanister will call the given callback,
/// or when the timeout reached.
///
/// 2. ProxyCanister stores request params, and waits until ProxyCanisterClient query and execute the request.
///
/// 3. ProxyCanister notifies the current canister about the reponse, by calling the `callback_api_fn_name` API endpoint.
/// The notification should be forwarded to the `OnResponse` callback, returned from `Self::new(...)`.
#[derive(Debug)]
pub struct NonReplicatedHttpOutcall {
requests: Rc<RefCell<HashMap<RequestId, DeferredResponse>>>,
callback_api_fn_name: &'static str,
proxy_canister: Principal,
request_timeout: Duration,
}

impl NonReplicatedHttpOutcall {
/// Crates a new instance of NonReplicatedHttpOutcall and a callback, which should be called
/// from canister API `callback_api_fn_name` function for response processing.
///
/// The `callback_api_fn_name` expected to
/// - have a name equal to `callback_api_fn_name` value,
/// - be a canister API update endpoint,
/// - have the following signature: `fn(Vec<ResponseResult>) -> ()`
/// - call the returned `OnResponse` callback.
pub fn new(
proxy_canister: Principal,
callback_api_fn_name: &'static str,
request_timeout: Duration,
) -> (Self, OnResponse) {
let s = Self {
requests: Default::default(),
callback_api_fn_name,
proxy_canister,
request_timeout,
};

let requests = Rc::clone(&s.requests);
let callback = Box::new(move |responses: Vec<ResponseResult>| {
for response in responses {
if let Some(deferred) = requests.borrow_mut().remove(&response.id) {
let _ = deferred.notify.send(response.result);
}
}
});

(s, callback)
}

/// Checks if some requests are expired, and, if so, finishs them with timeout error.
pub fn check_requests_timeout(&self) {
let now = ic::time();
self.requests
.borrow_mut()
.retain(|_, deffered| deffered.expired_at > now);
}
}

impl HttpOutcall for NonReplicatedHttpOutcall {
async fn request(&self, request: CanisterHttpRequestArgument) -> CallResult<HttpResponse> {
self.check_requests_timeout();

let proxy_canister = self.proxy_canister;
let request = RequestArgs {
callback_name: self.callback_api_fn_name.into(),
request,
};
let id: RequestId =
virtual_canister_call!(proxy_canister, REQUEST_METHOD_NAME, (request,), RequestId)
.await?;

let (notify, waker) = oneshot::channel();
let expired_at = ic::time() + self.request_timeout.as_nanos() as u64;
let response = DeferredResponse { expired_at, notify };
self.requests.borrow_mut().insert(id, response);

waker
.await
.map_err(|_| {
// if proxy canister doesn't respond
(
RejectionCode::SysTransient,
"timeout waiting HTTP request callback.".into(),
)
})?
.map_err(|e| {
// if request failed
(
RejectionCode::SysFatal,
format!("failed to send HTTP request: {e}"),
)
})
}
}

#[derive(Debug)]
struct DeferredResponse {
pub expired_at: u64,
pub notify: oneshot::Sender<Result<HttpResponse, String>>,
}
12 changes: 12 additions & 0 deletions ic-http-outcall/api/src/outcall.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
//! Abstraction over Http outcalls.

use ic_exports::ic_cdk::api::management_canister::http_request::{
CanisterHttpRequestArgument, HttpResponse,
};
use ic_exports::ic_kit::CallResult;

/// Abstraction over Http outcalls.
#[allow(async_fn_in_trait)]
pub trait HttpOutcall {
async fn request(&self, args: CanisterHttpRequestArgument) -> CallResult<HttpResponse>;
}
39 changes: 39 additions & 0 deletions ic-http-outcall/api/src/proxy_types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
use candid::Principal;
use ic_exports::candid::CandidType;
use ic_exports::ic_cdk::api::management_canister::http_request::{
CanisterHttpRequestArgument, HttpResponse,
};
use serde::{Deserialize, Serialize};

pub const REQUEST_METHOD_NAME: &str = "http_outcall";

/// Params for proxy canister initialization.
#[derive(Debug, Serialize, Deserialize, CandidType)]
pub struct InitArgs {
/// Off-chain proxy agent, allowed to perform http requests.
pub allowed_proxy: Principal,
}

/// ID of a request.
#[derive(
Debug, Clone, Copy, Serialize, Deserialize, CandidType, PartialEq, Eq, PartialOrd, Ord, Hash,
)]
pub struct RequestId(u64);

impl From<u64> for RequestId {
fn from(value: u64) -> Self {
Self(value)
}
}

#[derive(Debug, Clone, Deserialize, CandidType)]
pub struct RequestArgs {
pub callback_name: String,
pub request: CanisterHttpRequestArgument,
}

#[derive(Debug, Clone, Serialize, Deserialize, CandidType)]
pub struct ResponseResult {
pub id: RequestId,
pub result: Result<HttpResponse, String>,
}
16 changes: 16 additions & 0 deletions ic-http-outcall/api/src/replicated.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
use candid::Principal;
use ic_exports::ic_cdk::api::management_canister::http_request::{
CanisterHttpRequestArgument, HttpResponse,
};
use ic_exports::ic_kit::CallResult;
use ic_helpers::principal::management::ManagementPrincipalExt;

use crate::outcall::HttpOutcall;

pub struct ReplicatedHttpOutcall;

impl HttpOutcall for ReplicatedHttpOutcall {
async fn request(&self, args: CanisterHttpRequestArgument) -> CallResult<HttpResponse> {
Principal::management_canister().http_request(args).await
}
}
17 changes: 17 additions & 0 deletions ic-http-outcall/proxy-canister/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
[package]
name = "ic-http-outcall-proxy-canister"
version.workspace = true
edition.workspace = true

[features]
default = []
export-api = []

[dependencies]
ic-canister = { path = "../../ic-canister/ic-canister" }
ic-exports = { path = "../../ic-exports" }
ic-http-outcall-api = { path = "../api", features = ["proxy-api"] }


candid = { workspace = true }
serde = { workspace = true }
Loading