Skip to content
Merged
Show file tree
Hide file tree
Changes from 28 commits
Commits
Show all changes
36 commits
Select commit Hold shift + click to select a range
72901b9
feat: serialize json request body
gregorydemay Feb 28, 2025
e989118
refactor: move to inside http
gregorydemay Feb 28, 2025
ee9b6bc
refactor: json feature activate http
gregorydemay Feb 28, 2025
5749ade
refactor: use client with JSON RPC request
gregorydemay Feb 28, 2025
a81555b
refactor: use client with JSON RPC response
gregorydemay Feb 28, 2025
1f60ae6
fix: tests (start)
gregorydemay Feb 28, 2025
3e3b396
clippy, formatting
gregorydemay Mar 3, 2025
1fecc50
filter response
gregorydemay Mar 4, 2025
ff500a1
Use HTTP conversion as filter response
gregorydemay Mar 4, 2025
fce09ef
rename filter to convert
gregorydemay Mar 4, 2025
839b9c4
convert request service
gregorydemay Mar 4, 2025
c46b457
rename generic parameters
gregorydemay Mar 4, 2025
0430d63
use converter for JSON RPC request
gregorydemay Mar 4, 2025
74f2f88
use converter for JSON RPC response
gregorydemay Mar 4, 2025
d63226d
use converter for HTTP request
gregorydemay Mar 4, 2025
dded3a4
use converter for HTTP response
gregorydemay Mar 4, 2025
3fef789
clean-up canhttp
gregorydemay Mar 4, 2025
c54ee98
use explicit error
gregorydemay Mar 4, 2025
afb732e
remove filter feature
gregorydemay Mar 4, 2025
911a4f1
Filter HTTP response
gregorydemay Mar 4, 2025
10d5676
Use response filter
gregorydemay Mar 4, 2025
ea3b788
fix json comparison
gregorydemay Mar 4, 2025
258ddbe
simplify call
gregorydemay Mar 4, 2025
955170f
added TODOs
gregorydemay Mar 4, 2025
3ba8602
fix docs
gregorydemay Mar 4, 2025
34ab9df
lint
gregorydemay Mar 4, 2025
c75550e
split convert module between request/response
gregorydemay Mar 4, 2025
bb8b111
Merge branch 'main' into gdemay/XC-287-json-rpc-layer
gregorydemay Mar 4, 2025
c608a04
doc for convert module
gregorydemay Mar 6, 2025
1cd3403
doc for http module
gregorydemay Mar 6, 2025
708c9fc
make JsonRequestConverter generic
gregorydemay Mar 6, 2025
98fdb2e
docs for JSON module
gregorydemay Mar 6, 2025
5bac12c
docs for client
gregorydemay Mar 6, 2025
55db8d9
use `all-features` flag
gregorydemay Mar 6, 2025
bb68f2c
linting
gregorydemay Mar 6, 2025
1a4e772
unit tests for JSON RPC layer
gregorydemay Mar 6, 2025
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
4 changes: 4 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 7 additions & 5 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ inherits = "release"

[dependencies]
candid = { workspace = true }
canhttp = { path = "canhttp" }
canhttp = { path = "canhttp", features = ["json"] }
ethnum = { workspace = true }
evm_rpc_types = { path = "evm_rpc_types" }
futures = { workspace = true }
Expand All @@ -36,9 +36,10 @@ minicbor = { workspace = true }
serde = { workspace = true }
serde_bytes = { workspace = true }
serde_json = { workspace = true }
thiserror = { workspace = true }
thousands = "0.2"
tower = {workspace = true}
tower-http = {workspace = true, features = ["set-header", "util"]}
tower = { workspace = true }
tower-http = { workspace = true, features = ["set-header", "util"] }
url = { workspace = true }
hex = "0.4"
ethers-core = "2.0"
Expand All @@ -59,9 +60,10 @@ rand = "0.8"
[workspace.dependencies]
assert_matches = "1.5.0"
candid = { version = "0.10.13" }
candid_parser = {version = "0.1.4"}
candid_parser = { version = "0.1.4" }
ethnum = { version = "1.5.0", features = ["serde"] }
futures = "0.3.31"
futures-util = "0.3.31"
getrandom = { version = "0.2", features = ["custom"] }
hex = "0.4.3"
http = "1.2.0"
Expand Down Expand Up @@ -90,4 +92,4 @@ thiserror = "2.0.11"
url = "2.5"

[workspace]
members = [ "canhttp", "e2e/rust", "evm_rpc_types"]
members = ["canhttp", "e2e/rust", "evm_rpc_types"]
8 changes: 6 additions & 2 deletions canhttp/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@ documentation = "https://docs.rs/canhttp"

[dependencies]
ic-cdk = { workspace = true }
futures-util = { workspace = true }
http = { workspace = true, optional = true }
num-traits = { workspace = true, optional = true }
pin-project = { workspace = true }
tower = { workspace = true, features = ["filter", "retry"] }
serde = { workspace = true, optional = true }
serde_json = { workspace = true, optional = true }
tower = { workspace = true, features = ["retry"] }
tower-layer = { workspace = true, optional = true }
thiserror = { workspace = true }

Expand All @@ -26,4 +29,5 @@ tokio = { workspace = true, features = ["full"] }

[features]
default = ["http"]
http = ["dep:http", "dep:num-traits", "dep:tower-layer"]
http = ["dep:http", "dep:num-traits", "dep:tower-layer"]
json = ["http", "dep:serde", "dep:serde_json"]
22 changes: 18 additions & 4 deletions canhttp/src/client/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use thiserror::Error;
use tower::{BoxError, Service};
use tower::{BoxError, Service, ServiceBuilder};

/// Thin wrapper around [`ic_cdk::api::management_canister::http_request::http_request`]
/// that implements the [`tower::Service`] trait. Its functionality can be extended by composing so-called
Expand All @@ -19,6 +19,20 @@ use tower::{BoxError, Service};
#[derive(Clone, Debug)]
pub struct Client;

impl Client {
pub fn new_with_error<CustomError: From<IcError>>(

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The idea is that the service uses the CustomError type, so that any additional layer that may produce errors need to be mapped to the service error type. This forces the user of the library to handle errors explicitly.

) -> impl Service<IcHttpRequestWithCycles, Response = IcHttpResponse, Error = CustomError> {
ServiceBuilder::new()
.map_err(CustomError::from)
.service(Client)
}

pub fn new_with_box_error(
) -> impl Service<IcHttpRequestWithCycles, Response = IcHttpResponse, Error = BoxError> {
Self::new_with_error::<BoxError>()
}
}

/// Error returned by the Internet Computer when making an HTTPs outcall.
#[derive(Error, Clone, Debug, PartialEq, Eq)]
#[error("Error from ICP: (code {code:?}, message {message})")]
Expand All @@ -42,7 +56,7 @@ impl IcError {

impl Service<IcHttpRequestWithCycles> for Client {
type Response = IcHttpResponse;
type Error = BoxError;
type Error = IcError;
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>;

fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Expand All @@ -58,13 +72,13 @@ impl Service<IcHttpRequestWithCycles> for Client {
.await
{
Ok((response,)) => Ok(response),
Err((code, message)) => Err(BoxError::from(IcError { code, message })),
Err((code, message)) => Err(IcError { code, message }),
}
})
}
}

#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct IcHttpRequestWithCycles {
pub request: IcHttpRequest,
pub cycles: u128,
Expand Down
33 changes: 33 additions & 0 deletions canhttp/src/convert/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
pub use request::{ConvertRequest, ConvertRequestLayer};
pub use response::{ConvertResponse, ConvertResponseLayer};

mod request;
mod response;

use tower::ServiceBuilder;
use tower_layer::Stack;

pub trait Convert<Input> {
Comment thread
ninegua marked this conversation as resolved.
type Output;
type Error;

fn try_convert(&mut self, response: Input) -> Result<Self::Output, Self::Error>;
Comment thread
gregorydemay marked this conversation as resolved.
}

pub trait ConvertServiceBuilder<L> {
Comment thread
gregorydemay marked this conversation as resolved.
fn convert_request<C>(self, f: C) -> ServiceBuilder<Stack<ConvertRequestLayer<C>, L>>;
fn convert_response<C>(self, f: C) -> ServiceBuilder<Stack<ConvertResponseLayer<C>, L>>;
}

impl<L> ConvertServiceBuilder<L> for ServiceBuilder<L> {
fn convert_request<C>(self, converter: C) -> ServiceBuilder<Stack<ConvertRequestLayer<C>, L>> {
self.layer(ConvertRequestLayer::new(converter))
}

fn convert_response<C>(
self,
converter: C,
) -> ServiceBuilder<Stack<ConvertResponseLayer<C>, L>> {
self.layer(ConvertResponseLayer::new(converter))
}
}
55 changes: 55 additions & 0 deletions canhttp/src/convert/request.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
use crate::convert::Convert;
use futures_util::future;
use std::task::{Context, Poll};
use tower::Service;
use tower_layer::Layer;

#[derive(Debug, Clone)]
pub struct ConvertRequestLayer<C> {
converter: C,
}

impl<C> ConvertRequestLayer<C> {
pub fn new(converter: C) -> Self {
Self { converter }
}
}

#[derive(Debug, Clone)]
pub struct ConvertRequest<S, C> {
inner: S,
converter: C,
}

impl<S, C: Clone> Layer<S> for ConvertRequestLayer<C> {
type Service = ConvertRequest<S, C>;

fn layer(&self, inner: S) -> Self::Service {
Self::Service {
inner,
converter: self.converter.clone(),
}
}
}

impl<S, Converter, Request, NewRequest, Error> Service<NewRequest> for ConvertRequest<S, Converter>
where
Converter: Convert<NewRequest, Output = Request>,
S: Service<Request, Error = Error>,
Converter::Error: Into<Error>,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We cannot use tower::filter::Predicate because the use of BoxError is hard-coded.

{
type Response = S::Response;
type Error = S::Error;
type Future = future::Either<S::Future, future::Ready<Result<S::Response, S::Error>>>;

fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}

fn call(&mut self, new_req: NewRequest) -> Self::Future {
match self.converter.try_convert(new_req) {
Ok(request) => future::Either::Left(self.inner.call(request)),
Err(err) => future::Either::Right(future::ready(Err(err.into()))),
}
}
}
87 changes: 87 additions & 0 deletions canhttp/src/convert/response.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
use crate::convert::Convert;
use pin_project::pin_project;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use tower::Service;
use tower_layer::Layer;

#[derive(Debug, Clone)]
pub struct ConvertResponseLayer<C> {
converter: C,
}

impl<C> ConvertResponseLayer<C> {
pub fn new(converter: C) -> Self {
Self { converter }
}
}

#[derive(Debug, Clone)]
pub struct ConvertResponse<S, C> {
inner: S,
converter: C,
}

impl<S, C: Clone> Layer<S> for ConvertResponseLayer<C> {
type Service = ConvertResponse<S, C>;

fn layer(&self, inner: S) -> Self::Service {
Self::Service {
inner,
converter: self.converter.clone(),
}
}
}

impl<S, Request, Response, NewResponse, Converter> Service<Request>
for ConvertResponse<S, Converter>
where
S: Service<Request, Response = Response>,
Converter: Convert<Response, Output = NewResponse> + Clone,
Converter::Error: Into<S::Error>,
{
type Response = NewResponse;
type Error = S::Error;
type Future = ResponseFuture<S::Future, Converter>;

fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.inner.poll_ready(cx)
}

fn call(&mut self, req: Request) -> Self::Future {
ResponseFuture {
response_future: self.inner.call(req),
converter: self.converter.clone(),
}
}
}

#[pin_project]
pub struct ResponseFuture<F, Converter> {
#[pin]
response_future: F,
converter: Converter,
}
impl<F, Filter, Response, NewResponse, Error> Future for ResponseFuture<F, Filter>
where
F: Future<Output = Result<Response, Error>>,
Filter: Convert<Response, Output = NewResponse>,
Filter::Error: Into<Error>,
{
type Output = Result<NewResponse, Error>;

fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let result_fut = this.response_future.poll(cx);
match result_fut {
Poll::Ready(result) => match result {
Ok(response) => {
Poll::Ready(this.converter.try_convert(response).map_err(Into::into))
}
Err(err) => Poll::Ready(Err(err)),
},
Poll::Pending => Poll::Pending,
}
}
}
19 changes: 11 additions & 8 deletions canhttp/src/cycles/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,9 @@
mod tests;

use crate::client::IcHttpRequestWithCycles;
use crate::convert::Convert;
use ic_cdk::api::management_canister::http_request::CanisterHttpRequestArgument;
use thiserror::Error;
use tower::filter::Predicate;
use tower::BoxError;

/// Estimate the amount of cycles to charge for a single HTTPs outcall.
pub trait CyclesChargingPolicy {
Expand Down Expand Up @@ -97,7 +96,7 @@ impl CyclesCostEstimator {
}

/// Error return by the [`CyclesAccounting`] middleware.
#[derive(Error, Debug)]
#[derive(Error, Clone, Debug, PartialEq, Eq)]
pub enum CyclesAccountingError {
/// Error returned when the caller should be charged but did not attach sufficiently many cycles.
#[error("insufficient cycles (expected {expected:?}, received {received:?})")]
Expand Down Expand Up @@ -127,24 +126,28 @@ impl<Charging> CyclesAccounting<Charging> {
}
}

impl<CyclesEstimator> Predicate<CanisterHttpRequestArgument> for CyclesAccounting<CyclesEstimator>
impl<CyclesEstimator> Convert<CanisterHttpRequestArgument> for CyclesAccounting<CyclesEstimator>
where
CyclesEstimator: CyclesChargingPolicy,
{
type Request = IcHttpRequestWithCycles;
type Output = IcHttpRequestWithCycles;
type Error = CyclesAccountingError;

fn check(&mut self, request: CanisterHttpRequestArgument) -> Result<Self::Request, BoxError> {
fn try_convert(
&mut self,
request: CanisterHttpRequestArgument,
) -> Result<Self::Output, Self::Error> {
let cycles_to_attach = self.cycles_cost_estimator.cost_of_http_request(&request);
let cycles_to_charge = self
.charging_policy
.cycles_to_charge(&request, cycles_to_attach);
if cycles_to_charge > 0 {
let cycles_available = ic_cdk::api::call::msg_cycles_available128();
if cycles_available < cycles_to_charge {
return Err(Box::new(CyclesAccountingError::InsufficientCyclesError {
return Err(CyclesAccountingError::InsufficientCyclesError {
expected: cycles_to_charge,
received: cycles_available,
}));
});
}
let cycles_received = ic_cdk::api::call::msg_cycles_accept128(cycles_to_charge);
assert_eq!(
Expand Down
9 changes: 9 additions & 0 deletions canhttp/src/http/json/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
pub use request::{
HttpJsonRpcRequest, JsonRequestConversionError, JsonRequestConverter, JsonRpcRequestBody,
};
pub use response::{
HttpJsonRpcResponse, JsonResponseConversionError, JsonResponseConverter, JsonRpcResult,
};

mod request;
mod response;
Comment thread
gregorydemay marked this conversation as resolved.
Loading