-
Notifications
You must be signed in to change notification settings - Fork 20
feat: add EVM RPC canister client #447
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 12 commits
Commits
Show all changes
29 commits
Select commit
Hold shift + click to select a range
4d2c5a5
XC-412: Initialize new empty `evm_rpc_client` crate
lpahlavi d10ee6a
XC-412: Add skeleton for client
lpahlavi 4b71bd1
XC-412: Add `eth_getLogs` to new client
lpahlavi 07c8712
XC-412: Add empty changelog
lpahlavi d5d3413
XC-412: Add more type conversions to `alloy`
lpahlavi 99d82b2
XC-412: Use `try_from` instead of `from`
lpahlavi 34c70b9
XC-412: Add `get_logs` to client
lpahlavi fb4a545
XC-412: Don't expose private types
lpahlavi 041a47b
XC-412: Remove new `ProviderId` type
lpahlavi 33e50df
XC-412: Refactor `and_then`
lpahlavi 4619b97
XC-412: Make `f` mutable
lpahlavi 5e8aeed
XC-412: Fix repository link
lpahlavi fff88e0
XC-412: Require docs
lpahlavi a547409
XC-412: Add rustdoc and examples
lpahlavi 5e5c55a
XC-412: Move type conversions to separate files
lpahlavi 4a73233
XC-412: Flesh out examples
lpahlavi 99d031c
XC-412: Move more type conversions to separate files
lpahlavi ab47bb8
XC-412: Add TODO for conversion from `alloy_rpc_types::Filter` to `Ge…
lpahlavi 59801f2
XC-412: Add some alloy conversion unit tests
lpahlavi ccb0f5d
XC-412: Add unit tests for `and_then` method
lpahlavi 438dd53
Merge branch 'main' into lpahlavi/XC-412-evm-rpc-client
lpahlavi b152927
XC-412: Formatting
lpahlavi 5de395d
XC-412: Add more unit tests
lpahlavi db3d6f5
XC-412: Improve rustdocs
lpahlavi 71b37d4
Merge branch 'main' into lpahlavi/XC-412-evm-rpc-client
lpahlavi c729d72
XC-412: Add `collapse` to `ande_then`
lpahlavi 9a97a60
XC-412: Add explanation for `hex_to_u32_digits`
lpahlavi ee6ab99
XC-412: Add unit tests for error consolidation
lpahlavi 3efc9c1
XC-412: FOrMaTtInG
lpahlavi 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
Large diffs are not rendered by default.
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,23 @@ | ||
| [package] | ||
| name = "evm_rpc_client" | ||
| version = "1.4.0" | ||
| description = "Rust client for interacting with the EVM RPC canister" | ||
| license = "Apache-2.0" | ||
| readme = "README.md" | ||
| authors = ["DFINITY Foundation"] | ||
| edition = "2021" | ||
| include = ["src", "Cargo.toml", "CHANGELOG.md", "LICENSE", "README.md"] | ||
| repository = "https://github.com/dfinity/evm-rpc-canister" | ||
| documentation = "https://docs.rs/evm_rpc_client" | ||
|
|
||
| [dependencies] | ||
| alloy-rpc-types = { workspace = true } | ||
| async-trait = { workspace = true } | ||
| candid = { workspace = true } | ||
| evm_rpc_types = { path = "../evm_rpc_types", features = ["alloy"] } | ||
| ic-cdk = { workspace = true } | ||
| ic-error-types = { workspace = true } | ||
| serde = { workspace = true } | ||
| strum = { workspace = true } | ||
|
|
||
| [dev-dependencies] |
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 @@ | ||
| ../NOTICE |
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,3 @@ | ||
| # EVM RPC Client | ||
|
|
||
| This crate defines a client for interacting with the EVM RPC canister. |
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,310 @@ | ||
| //! TODO XC-412: Add documentation and examples | ||
|
|
||
| mod request; | ||
|
|
||
| use crate::request::{Request, RequestBuilder}; | ||
| use async_trait::async_trait; | ||
| use candid::utils::ArgumentEncoder; | ||
| use candid::{CandidType, Principal}; | ||
| use evm_rpc_types::{ConsensusStrategy, GetLogsArgs, RpcConfig, RpcServices}; | ||
| use ic_cdk::api::call::RejectionCode as IcCdkRejectionCode; | ||
| use ic_error_types::RejectCode; | ||
| use request::{GetLogsRequest, GetLogsRequestBuilder}; | ||
| use serde::de::DeserializeOwned; | ||
| use std::sync::Arc; | ||
|
|
||
| /// The principal identifying the productive EVM RPC canister under NNS control. | ||
| /// | ||
| /// ```rust | ||
| /// use candid::Principal; | ||
| /// use evm_rpc_client::EVM_RPC_CANISTER; | ||
| /// | ||
| /// assert_eq!(EVM_RPC_CANISTER, Principal::from_text("7hfb6-caaaa-aaaar-qadga-cai").unwrap()) | ||
| /// ``` | ||
| pub const EVM_RPC_CANISTER: Principal = Principal::from_slice(&[0, 0, 0, 0, 2, 48, 0, 204, 1, 1]); | ||
|
|
||
| /// Abstract the canister runtime so that the client code can be reused: | ||
| /// * in production using `ic_cdk`, | ||
| /// * in unit tests by mocking this trait, | ||
| /// * in integration tests by implementing this trait for `PocketIc`. | ||
| #[async_trait] | ||
| pub trait Runtime { | ||
| /// Defines how asynchronous inter-canister update calls are made. | ||
| async fn update_call<In, Out>( | ||
| &self, | ||
| id: Principal, | ||
| method: &str, | ||
| args: In, | ||
| cycles: u128, | ||
| ) -> Result<Out, (RejectCode, String)> | ||
| where | ||
| In: ArgumentEncoder + Send, | ||
| Out: CandidType + DeserializeOwned; | ||
|
|
||
| /// Defines how asynchronous inter-canister query calls are made. | ||
| async fn query_call<In, Out>( | ||
| &self, | ||
| id: Principal, | ||
| method: &str, | ||
| args: In, | ||
| ) -> Result<Out, (RejectCode, String)> | ||
| where | ||
| In: ArgumentEncoder + Send, | ||
| Out: CandidType + DeserializeOwned; | ||
| } | ||
|
|
||
| /// Client to interact with the EVM RPC canister. | ||
| #[derive(Debug)] | ||
| pub struct EvmRpcClient<R> { | ||
| config: Arc<ClientConfig<R>>, | ||
| } | ||
|
|
||
| impl<R> Clone for EvmRpcClient<R> { | ||
| fn clone(&self) -> Self { | ||
| Self { | ||
| config: self.config.clone(), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl<R> EvmRpcClient<R> { | ||
| /// Creates a [`ClientBuilder`] to configure a [`EvmRpcClient`]. | ||
| pub fn builder(runtime: R, evm_rpc_canister: Principal) -> ClientBuilder<R> { | ||
| ClientBuilder::new(runtime, evm_rpc_canister) | ||
| } | ||
|
|
||
| /// Returns a reference to the client's runtime. | ||
| pub fn runtime(&self) -> &R { | ||
| &self.config.runtime | ||
| } | ||
| } | ||
|
|
||
| impl EvmRpcClient<IcRuntime> { | ||
| /// Creates a [`ClientBuilder`] to configure a [`EvmRpcClient`] targeting [`EVM_RPC_CANISTER`] | ||
| /// running on the Internet Computer. | ||
| pub fn builder_for_ic() -> ClientBuilder<IcRuntime> { | ||
| ClientBuilder::new(IcRuntime, EVM_RPC_CANISTER) | ||
| } | ||
| } | ||
|
|
||
| /// Client to interact with the EVM RPC canister. | ||
lpahlavi marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| #[derive(Clone, Eq, PartialEq, Debug)] | ||
| pub struct ClientConfig<R> { | ||
| runtime: R, | ||
| evm_rpc_canister: Principal, | ||
| rpc_config: Option<RpcConfig>, | ||
| rpc_services: RpcServices, | ||
| } | ||
|
|
||
| /// A [`ClientBuilder`] to create a [`EvmRpcClient`] with custom configuration. | ||
| #[must_use] | ||
| pub struct ClientBuilder<R> { | ||
| config: ClientConfig<R>, | ||
| } | ||
|
|
||
| impl<R> ClientBuilder<R> { | ||
| fn new(runtime: R, evm_rpc_canister: Principal) -> Self { | ||
| Self { | ||
| config: ClientConfig { | ||
| runtime, | ||
| evm_rpc_canister, | ||
| rpc_config: None, | ||
| rpc_services: RpcServices::EthMainnet(None), | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| /// Modify the existing runtime by applying a transformation function. | ||
| /// | ||
| /// The transformation does not necessarily produce a runtime of the same type. | ||
| pub fn with_runtime<S, F: FnOnce(R) -> S>(self, other_runtime: F) -> ClientBuilder<S> { | ||
gregorydemay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| ClientBuilder { | ||
| config: ClientConfig { | ||
| runtime: other_runtime(self.config.runtime), | ||
| evm_rpc_canister: self.config.evm_rpc_canister, | ||
| rpc_config: self.config.rpc_config, | ||
| rpc_services: self.config.rpc_services, | ||
| }, | ||
| } | ||
| } | ||
|
|
||
| /// Mutates the builder to use the given [`RpcServices`]. | ||
| pub fn with_rpc_sources(mut self, rpc_services: RpcServices) -> Self { | ||
| self.config.rpc_services = rpc_services; | ||
| self | ||
| } | ||
|
|
||
| /// Mutates the builder to use the given [`RpcConfig`]. | ||
| pub fn with_rpc_config(mut self, rpc_config: RpcConfig) -> Self { | ||
| self.config.rpc_config = Some(rpc_config); | ||
| self | ||
| } | ||
|
|
||
| /// Mutates the builder to use the given [`ConsensusStrategy`] in the [`RpcConfig`]. | ||
| pub fn with_consensus_strategy(mut self, consensus_strategy: ConsensusStrategy) -> Self { | ||
| self.config.rpc_config = Some(RpcConfig { | ||
| response_consensus: Some(consensus_strategy), | ||
| ..self.config.rpc_config.unwrap_or_default() | ||
| }); | ||
| self | ||
| } | ||
|
|
||
| /// Mutates the builder to use the given `response_size_estimate` in the [`RpcConfig`]. | ||
| pub fn with_response_size_estimate(mut self, response_size_estimate: u64) -> Self { | ||
| self.config.rpc_config = Some(RpcConfig { | ||
| response_size_estimate: Some(response_size_estimate), | ||
| ..self.config.rpc_config.unwrap_or_default() | ||
| }); | ||
| self | ||
| } | ||
|
|
||
| /// Creates a [`EvmRpcClient`] from the configuration specified in the [`ClientBuilder`]. | ||
| pub fn build(self) -> EvmRpcClient<R> { | ||
| EvmRpcClient { | ||
| config: Arc::new(self.config), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| impl<R> EvmRpcClient<R> { | ||
| /// Call `get_ethLogs` on the EVM RPC canister. | ||
| /// TODO XC-412: Add docs and examples | ||
| pub fn get_logs(&self, params: impl Into<GetLogsArgs>) -> GetLogsRequestBuilder<R> { | ||
gregorydemay marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| RequestBuilder::new( | ||
| self.clone(), | ||
| GetLogsRequest::new(params.into()), | ||
| 10_000_000_000, | ||
| ) | ||
| } | ||
| } | ||
|
|
||
| impl<R: Runtime> EvmRpcClient<R> { | ||
| /// Call `getProviders` on the EVM RPC canister. | ||
| pub async fn get_providers(&self) -> Vec<evm_rpc_types::Provider> { | ||
| self.config | ||
| .runtime | ||
| .query_call(self.config.evm_rpc_canister, "getProviders", ()) | ||
| .await | ||
| .unwrap() | ||
| } | ||
|
|
||
| /// Call `getServiceProviderMap` on the EVM RPC canister. | ||
| // TODO XC-412: Create type alias in `evm_rpc_types` for `ProviderId` i.e. `u64` | ||
| pub async fn get_service_provider_map(&self) -> Vec<(evm_rpc_types::RpcService, u64)> { | ||
| self.config | ||
| .runtime | ||
| .query_call(self.config.evm_rpc_canister, "getServiceProviderMap", ()) | ||
| .await | ||
| .unwrap() | ||
| } | ||
|
|
||
| /// Call `updateApiKeys` on the EVM RPC canister. | ||
| // TODO XC-412: Create type alias in `evm_rpc_types` for `ProviderId` i.e. `u64` | ||
| pub async fn update_api_keys(&self, api_keys: &[(u64, Option<String>)]) { | ||
| self.config | ||
| .runtime | ||
| .update_call( | ||
| self.config.evm_rpc_canister, | ||
| "updateApiKeys", | ||
| (api_keys.to_vec(),), | ||
| 0, | ||
| ) | ||
| .await | ||
| .unwrap() | ||
| } | ||
|
|
||
| async fn execute_request<Config, Params, CandidOutput, Output>( | ||
| &self, | ||
| request: Request<Config, Params, CandidOutput, Output>, | ||
| ) -> Output | ||
| where | ||
| Config: CandidType + Send, | ||
| Params: CandidType + Send, | ||
| CandidOutput: Into<Output> + CandidType + DeserializeOwned, | ||
| { | ||
| let rpc_method = request.endpoint.rpc_method(); | ||
| self.try_execute_request(request) | ||
| .await | ||
| .unwrap_or_else(|e| panic!("Client error: failed to call `{}`: {e:?}", rpc_method)) | ||
| } | ||
|
|
||
| async fn try_execute_request<Config, Params, CandidOutput, Output>( | ||
| &self, | ||
| request: Request<Config, Params, CandidOutput, Output>, | ||
| ) -> Result<Output, (RejectCode, String)> | ||
| where | ||
| Config: CandidType + Send, | ||
| Params: CandidType + Send, | ||
| CandidOutput: Into<Output> + CandidType + DeserializeOwned, | ||
| { | ||
| self.config | ||
| .runtime | ||
| .update_call::<(RpcServices, Option<Config>, Params), CandidOutput>( | ||
| self.config.evm_rpc_canister, | ||
| request.endpoint.rpc_method(), | ||
| (request.rpc_services, request.rpc_config, request.params), | ||
| request.cycles, | ||
| ) | ||
| .await | ||
| .map(Into::into) | ||
| } | ||
| } | ||
|
|
||
| /// Runtime when interacting with a canister running on the Internet Computer. | ||
| #[derive(Copy, Clone, Eq, PartialEq, Debug)] | ||
| pub struct IcRuntime; | ||
|
|
||
| #[async_trait] | ||
| impl Runtime for IcRuntime { | ||
| async fn update_call<In, Out>( | ||
| &self, | ||
| id: Principal, | ||
| method: &str, | ||
| args: In, | ||
| cycles: u128, | ||
| ) -> Result<Out, (RejectCode, String)> | ||
| where | ||
| In: ArgumentEncoder + Send, | ||
| Out: CandidType + DeserializeOwned, | ||
| { | ||
| ic_cdk::api::call::call_with_payment128(id, method, args, cycles) | ||
| .await | ||
| .map(|(res,)| res) | ||
| .map_err(|(code, message)| (convert_reject_code(code), message)) | ||
| } | ||
|
|
||
| async fn query_call<In, Out>( | ||
| &self, | ||
| id: Principal, | ||
| method: &str, | ||
| args: In, | ||
| ) -> Result<Out, (RejectCode, String)> | ||
| where | ||
| In: ArgumentEncoder + Send, | ||
| Out: CandidType + DeserializeOwned, | ||
| { | ||
| ic_cdk::api::call::call(id, method, args) | ||
| .await | ||
| .map(|(res,)| res) | ||
| .map_err(|(code, message)| (convert_reject_code(code), message)) | ||
| } | ||
| } | ||
|
|
||
| fn convert_reject_code(code: IcCdkRejectionCode) -> RejectCode { | ||
| match code { | ||
| IcCdkRejectionCode::SysFatal => RejectCode::SysFatal, | ||
| IcCdkRejectionCode::SysTransient => RejectCode::SysTransient, | ||
| IcCdkRejectionCode::DestinationInvalid => RejectCode::DestinationInvalid, | ||
| IcCdkRejectionCode::CanisterReject => RejectCode::CanisterReject, | ||
| IcCdkRejectionCode::CanisterError => RejectCode::CanisterError, | ||
| IcCdkRejectionCode::Unknown => { | ||
| // This can only happen if there is a new error code on ICP that the CDK is not aware of. | ||
| // We map it to SysFatal since none of the other error codes apply. | ||
| // In particular, note that RejectCode::SysUnknown is only applicable to inter-canister calls that used ic0.call_with_best_effort_response. | ||
| RejectCode::SysFatal | ||
| } | ||
| IcCdkRejectionCode::NoError => { | ||
| unreachable!("inter-canister calls should never produce a RejectionCode::NoError error") | ||
| } | ||
| } | ||
| } | ||
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.