-
Notifications
You must be signed in to change notification settings - Fork 22
feat: library canhttp
#364
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
39 commits
Select commit
Hold shift + click to select a range
6935680
new crate canjsonrpc
gregorydemay 932ecb0
setup auxiliary files
gregorydemay a58d7f3
JSON RPC types
gregorydemay af1454d
request builder
gregorydemay e7dc79d
skeleton to execute request
gregorydemay 0ce804e
cycles charging
gregorydemay 7d9763b
retry strategy
gregorydemay c5d0dec
added request builder methods
gregorydemay 5863011
constructor JsonRpcRequest
gregorydemay 09daf7f
PoC with Tower
gregorydemay 84a00a9
copied over cycles cost calculation
gregorydemay c27f7a7
charge user as filter
gregorydemay f7929d5
retry policy if response too large
gregorydemay aedffaf
map request/response to JSON RPC
gregorydemay cf78f22
layered service HTTP -> ICP
gregorydemay dbd89d5
dump
gregorydemay 14627b7
Revert
gregorydemay 066016c
rename canhttp
gregorydemay 2236d82
basic cycles handling
gregorydemay d2c172f
IC client
gregorydemay 996e5c4
build client in evm_rpc_canister
gregorydemay 336d970
Merge branch 'main' into gdemay/XC-287-canjsonrpc
gregorydemay 1a8f6de
use client from canhttp
gregorydemay 66fb8ae
clippy
gregorydemay 431a1bb
move cycle accounting to canhttp
gregorydemay 54cde76
default implementation RequestCyclesCostWithCollateralEstimator
gregorydemay 8f1df2d
Put cycles accounting on a separate layer
gregorydemay 5492d68
docs
gregorydemay 9c4b59c
fix: use cycles to charge for request_cost
gregorydemay 8f244bb
formatting
gregorydemay cf88542
improve Rust docs of EstimateRequestCyclesCost
gregorydemay 4d994ce
improve panic message
gregorydemay eaba9b1
Apply suggestions from code review
gregorydemay 54d2789
Improve doc
gregorydemay f709fb5
Merge branch 'main' into gdemay/XC-287-canjsonrpc
gregorydemay 360ac72
remove trait cycles to attach
gregorydemay f75eef7
rename
gregorydemay 394ea3a
linting
gregorydemay f5c3dc6
fix CI
gregorydemay File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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] |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| [package] | ||
| name = "canhttp" | ||
| version = "0.1.0" | ||
| description = "Rust library to issue HTTPs outcalls from a canister on the Internet Computer" | ||
| license = "Apache-2.0" | ||
| readme = "README.md" | ||
| authors = ["DFINITY Foundation"] | ||
| edition = "2021" | ||
| include = ["src", "Cargo.toml", "CHANGELOG.md", "LICENSE", "README.md"] | ||
| documentation = "https://docs.rs/canhttp" | ||
|
|
||
| [dependencies] | ||
| ic-cdk = { workspace = true } | ||
| tower = { workspace = true, features = ["filter", "retry"] } | ||
| thiserror = { workspace = true } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| ../LICENSE |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| [](https://internetcomputer.org) | ||
| [](https://forum.dfinity.org/) | ||
| [](LICENSE) | ||
|
|
||
|
|
||
| # canhttp |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,69 @@ | ||
| use ic_cdk::api::call::RejectionCode; | ||
| use ic_cdk::api::management_canister::http_request::{ | ||
| CanisterHttpRequestArgument as IcHttpRequest, HttpResponse as IcHttpResponse, | ||
| }; | ||
| use std::future::Future; | ||
| use std::pin::Pin; | ||
| use std::task::{Context, Poll}; | ||
| use thiserror::Error; | ||
| use tower::{BoxError, Service}; | ||
|
|
||
| /// 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 | ||
| /// [tower middlewares](https://docs.rs/tower/latest/tower/#usage). | ||
| /// | ||
| /// Middlewares from this crate: | ||
| /// * [`crate::cycles::CyclesAccounting`]: handles cycles accounting. | ||
| #[derive(Clone, Debug)] | ||
| pub struct Client; | ||
|
|
||
| /// Error returned by the Internet Computer when making an HTTPs outcall. | ||
| #[derive(Error, Debug, PartialEq, Eq)] | ||
| #[error("Error from ICP: (code {code:?}, message {message})")] | ||
| pub struct IcError { | ||
| /// Rejection code as specified [here](https://internetcomputer.org/docs/current/references/ic-interface-spec#reject-codes) | ||
| pub code: RejectionCode, | ||
| /// Associated helper message. | ||
| pub message: String, | ||
| } | ||
|
|
||
| impl IcError { | ||
| /// Determines whether the error indicates that the response was larger than the specified | ||
| /// [`max_response_bytes`](https://internetcomputer.org/docs/current/references/ic-interface-spec#ic-http_request) specified in the request. | ||
| /// | ||
| /// If true, retrying with a larger value for `max_response_bytes` may help. | ||
| pub fn is_response_too_large(&self) -> bool { | ||
| self.code == RejectionCode::SysFatal | ||
| && (self.message.contains("size limit") || self.message.contains("length limit")) | ||
| } | ||
| } | ||
|
|
||
| impl Service<IcHttpRequestWithCycles> for Client { | ||
| type Response = IcHttpResponse; | ||
| type Error = BoxError; | ||
| type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>>>>; | ||
|
|
||
| fn poll_ready(&mut self, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { | ||
| Poll::Ready(Ok(())) | ||
| } | ||
|
|
||
| fn call( | ||
| &mut self, | ||
| IcHttpRequestWithCycles { request, cycles }: IcHttpRequestWithCycles, | ||
| ) -> Self::Future { | ||
| Box::pin(async move { | ||
| match ic_cdk::api::management_canister::http_request::http_request(request, cycles) | ||
| .await | ||
| { | ||
| Ok((response,)) => Ok(response), | ||
| Err((code, message)) => Err(BoxError::from(IcError { code, message })), | ||
| } | ||
| }) | ||
| } | ||
| } | ||
|
|
||
| #[derive(Clone, Debug, PartialEq, Eq)] | ||
| pub struct IcHttpRequestWithCycles { | ||
| pub request: IcHttpRequest, | ||
| pub cycles: u128, | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,160 @@ | ||
| #[cfg(test)] | ||
| mod tests; | ||
|
|
||
| use crate::client::IcHttpRequestWithCycles; | ||
| 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 { | ||
| /// Determine the amount of cycles to charge the caller. | ||
| /// | ||
| /// If the value is `0`, no cycles will be charged, meaning that the canister using that library will | ||
| /// pay for HTTPs outcalls with its own cycles. Otherwise, the returned amount of cycles will be transferred | ||
| /// from the caller to the canister's cycles balance to pay (in part or fully) for the HTTPs outcall. | ||
| fn cycles_to_charge( | ||
|
lpahlavi marked this conversation as resolved.
|
||
| &self, | ||
| _request: &CanisterHttpRequestArgument, | ||
| _attached_cycles: u128, | ||
| ) -> u128 { | ||
| 0 | ||
| } | ||
| } | ||
|
|
||
| /// Estimate the exact minimum cycles amount required to send an HTTPs outcall as specified | ||
| /// [here](https://internetcomputer.org/docs/current/developer-docs/gas-cost#https-outcalls). | ||
| #[derive(Debug, Clone, Eq, PartialEq)] | ||
| pub struct CyclesCostEstimator { | ||
| num_nodes_in_subnet: u32, | ||
|
lpahlavi marked this conversation as resolved.
|
||
| } | ||
|
|
||
| impl CyclesCostEstimator { | ||
| /// Maximum value for `max_response_bytes` which is 2MB, | ||
| /// see the [IC specification](https://internetcomputer.org/docs/current/references/ic-interface-spec#ic-http_request). | ||
| pub const DEFAULT_MAX_RESPONSE_BYTES: u64 = 2_000_000; | ||
|
|
||
| /// Create a new estimator for a subnet having the given number of nodes. | ||
| pub const fn new(num_nodes_in_subnet: u32) -> Self { | ||
| CyclesCostEstimator { | ||
| num_nodes_in_subnet, | ||
| } | ||
| } | ||
|
|
||
| /// Compute the number of cycles required to send the given request via HTTPs outcall. | ||
| /// | ||
| /// An HTTP outcall entails calling the `http_request` method on the management canister interface, | ||
| /// which requires that cycles to pay for the call must be explicitly attached with the call | ||
| /// ([IC specification](https://internetcomputer.org/docs/current/references/ic-interface-spec#ic-http_request)). | ||
| /// The required amount of cycles to attach is specified | ||
| /// [here](https://internetcomputer.org/docs/current/developer-docs/gas-cost#https-outcalls). | ||
| pub fn cost_of_http_request(&self, request: &CanisterHttpRequestArgument) -> u128 { | ||
| let payload_body_bytes = request | ||
| .body | ||
| .as_ref() | ||
| .map(|body| body.len()) | ||
| .unwrap_or_default(); | ||
| let extra_payload_bytes = request.url.len() | ||
| + request | ||
| .headers | ||
| .iter() | ||
| .map(|header| header.name.len() + header.value.len()) | ||
| .sum::<usize>() | ||
| + request.transform.as_ref().map_or(0, |transform| { | ||
| transform.function.0.method.len() + transform.context.len() | ||
| }); | ||
| let max_response_bytes = request | ||
| .max_response_bytes | ||
| .unwrap_or(Self::DEFAULT_MAX_RESPONSE_BYTES); | ||
| let request_bytes = (payload_body_bytes + extra_payload_bytes) as u128; | ||
| self.base_fee() | ||
| + self.request_fee(request_bytes) | ||
| + self.response_fee(max_response_bytes as u128) | ||
| } | ||
|
|
||
| fn base_fee(&self) -> u128 { | ||
| 3_000_000_u128 | ||
| .saturating_add(60_000_u128.saturating_mul(self.num_nodes_as_u128())) | ||
| .saturating_mul(self.num_nodes_as_u128()) | ||
| } | ||
|
|
||
| fn request_fee(&self, bytes: u128) -> u128 { | ||
| 400_u128 | ||
| .saturating_mul(self.num_nodes_as_u128()) | ||
| .saturating_mul(bytes) | ||
| } | ||
|
|
||
| fn response_fee(&self, bytes: u128) -> u128 { | ||
| 800_u128 | ||
| .saturating_mul(self.num_nodes_as_u128()) | ||
| .saturating_mul(bytes) | ||
| } | ||
|
|
||
| fn num_nodes_as_u128(&self) -> u128 { | ||
| self.num_nodes_in_subnet as u128 | ||
| } | ||
| } | ||
|
|
||
| /// Error return by the [`CyclesAccounting`] middleware. | ||
| #[derive(Error, Debug)] | ||
| 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:?})")] | ||
| InsufficientCyclesError { | ||
| /// Expected amount of cycles. Minimum value that should have been sent. | ||
| expected: u128, | ||
| /// Received amount of cycles | ||
| received: u128, | ||
| }, | ||
| } | ||
|
|
||
| /// A middleware to handle cycles accounting, i.e. verify if sufficiently many cycles are available in a request. | ||
| /// How cycles are estimated is given by `CyclesEstimator` | ||
| #[derive(Clone, Debug)] | ||
| pub struct CyclesAccounting<Charging> { | ||
| cycles_cost_estimator: CyclesCostEstimator, | ||
| charging_policy: Charging, | ||
| } | ||
|
|
||
| impl<Charging> CyclesAccounting<Charging> { | ||
| /// Create a new middleware given the cycles estimator. | ||
| pub fn new(num_nodes_in_subnet: u32, charging_policy: Charging) -> Self { | ||
| Self { | ||
| cycles_cost_estimator: CyclesCostEstimator::new(num_nodes_in_subnet), | ||
| charging_policy, | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl<CyclesEstimator> Predicate<CanisterHttpRequestArgument> for CyclesAccounting<CyclesEstimator> | ||
| where | ||
| CyclesEstimator: CyclesChargingPolicy, | ||
| { | ||
| type Request = IcHttpRequestWithCycles; | ||
|
|
||
| fn check(&mut self, request: CanisterHttpRequestArgument) -> Result<Self::Request, BoxError> { | ||
| 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 { | ||
| expected: cycles_to_charge, | ||
| received: cycles_available, | ||
| })); | ||
| } | ||
| let cycles_received = ic_cdk::api::call::msg_cycles_accept128(cycles_to_charge); | ||
| assert_eq!( | ||
| cycles_received, cycles_to_charge, | ||
| "Expected to receive {cycles_to_charge}, but got {cycles_received}" | ||
| ); | ||
|
lpahlavi marked this conversation as resolved.
|
||
| } | ||
| Ok(IcHttpRequestWithCycles { | ||
| request, | ||
| cycles: cycles_to_attach, | ||
|
lpahlavi marked this conversation as resolved.
|
||
| }) | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.