Skip to content
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

Experiment: Add individual RPC call methods #39

Closed
wants to merge 15 commits into from
1,317 changes: 1,106 additions & 211 deletions Cargo.lock

Large diffs are not rendered by default.

16 changes: 10 additions & 6 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,22 +16,26 @@ candid = "0.9"
ic-canister-log = { git = "https://github.com/dfinity/ic", rev = "release-2023-08-30_23-01" }
ic-canisters-http-types = { git = "https://github.com/dfinity/ic", rev = "release-2023-08-30_23-01" }
# ic-nervous-system-common = { git = "https://github.com/dfinity/ic", rev = "release-2023-08-30_23-01" }
ic-certified-map = "0.4"
# ic-certified-map = "0.4"
ic-cdk = "0.10"
ic-cdk-macros = "0.7"
ic-eth = "0.0"
ic-eth = "0.1"
ic-metrics-encoder = "1.1"
ic-stable-structures = "0.5"
json5 = "0.4"
# ethers-providers = { git = "https://github.com/rvanasa/ethers-rs", branch = "ic-ethers", default-features = false }
# ethers-providers = { path = "../ethers-rs/ethers-providers", default-features = false }
ethers-providers = "2.0"
async-trait = "0.1"
num = "0.4"
num-traits = "0.2"
num-derive = "0.4"
serde = "1"
serde_bytes = "0.11"
serde_cbor = "0.11"
sha2 = "0.10"
serde_json = "1.0"
url = "2.4"

[dev-dependencies]
candid = { features = ["parser"] }
hex = "0.4"

[patch.crates-io]
wasm-bindgen = { path = "../wasm-bindgen", package = "ic-wasm-bindgen" }
10 changes: 6 additions & 4 deletions candid/ic_eth.did
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ type EthRpcError = variant {
HttpRequestError : record { code : nat32; message : text };
TooFewCycles : text;
ServiceUrlParseError;
SerializeError;
ServiceUrlHostMissing;
ProviderNotFound;
NoPermission;
Expand All @@ -25,8 +26,8 @@ type RegisterProvider = record {
cycles_per_call : nat64;
credential_path : text;
};
type Result = variant { Ok : vec nat8; Err : EthRpcError };
type Result_1 = variant { Ok : nat; Err : EthRpcError };
type Result = variant { Ok : nat; Err : EthRpcError };
type Result_1 = variant { Ok : vec nat8; Err : EthRpcError };
type Source = variant { Url : text; Chain : nat64; Provider : nat64 };
type UpdateProvider = record {
base_url : opt text;
Expand All @@ -39,14 +40,15 @@ type UpdateProvider = record {
service : {
authorize : (principal, Auth) -> ();
deauthorize : (principal, Auth) -> ();
eth_gas_price : (Source) -> (Result);
get_authorized : (Auth) -> (vec text) query;
get_nodes_in_subnet : () -> (nat32) query;
get_open_rpc_access : () -> (bool) query;
get_owed_cycles : (nat64) -> (nat) query;
get_providers : () -> (vec ProviderView) query;
register_provider : (RegisterProvider) -> (nat64);
request : (Source, text, nat64) -> (Result);
request_cost : (Source, text, nat64) -> (Result_1) query;
request : (Source, text, nat64) -> (Result_1);
request_cost : (Source, text, nat64) -> (Result) query;
set_nodes_in_subnet : (nat32) -> ();
set_open_rpc_access : (bool) -> ();
unregister_provider : (nat64) -> ();
Expand Down
2 changes: 1 addition & 1 deletion dfx.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
{
"canisters": {
"ic_eth": {
"candid": "candid/ic_eth.did",
"type": "rust",
"candid": "candid/ic_eth.did",
"package": "ic_eth_rpc"
}
},
Expand Down
24 changes: 22 additions & 2 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,34 @@ use ic_cdk::api::management_canister::http_request::{
http_request as make_http_request, CanisterHttpRequestArgument, HttpHeader, HttpMethod,
TransformContext,
};
use ic_eth::rpc::JsonRpcRequest;
use serde::{de::DeserializeOwned, Serialize};

use crate::*;

pub async fn do_http_request(
pub async fn do_http_request<T: Serialize, R: DeserializeOwned>(
source: ResolvedSource,
method: &str,
params: T,
max_response_bytes: u64,
) -> Result<R> {
serde_json::from_slice(
&do_http_request_str(
source,
&serde_json::to_string(&JsonRpcRequest::new(method, params))
.map_err(|_| EthRpcError::SerializeError)?,
max_response_bytes,
)
.await?,
)
.or(Err(EthRpcError::SerializeError))
}

pub async fn do_http_request_str(
source: ResolvedSource,
json_rpc_payload: &str,
max_response_bytes: u64,
) -> Result<Vec<u8>, EthRpcError> {
) -> Result<Vec<u8>> {
inc_metric!(requests);
if !is_authorized(Auth::Rpc) {
inc_metric!(request_err_no_permission);
Expand Down
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ mod constants;
mod http;
mod memory;
mod metrics;
mod rpc;
mod signature;
mod types;
mod util;
Expand All @@ -17,6 +18,7 @@ pub use crate::constants::*;
pub use crate::http::*;
pub use crate::memory::*;
pub use crate::metrics::*;
pub use crate::rpc::*;
pub use crate::signature::*;
pub use crate::types::*;
pub use crate::util::*;
Expand Down
51 changes: 42 additions & 9 deletions src/main.rs
Original file line number Diff line number Diff line change
@@ -1,37 +1,70 @@
use candid::{candid_method, CandidType};
use ethers_providers::Middleware;
use ic_canister_log::log;
use ic_cdk::api::management_canister::http_request::{HttpHeader, HttpResponse, TransformArgs};
use ic_cdk::{query, update};
use ic_eth_rpc::*;

// use ic_canisters_http_types::{
// HttpRequest as AssetHttpRequest, HttpResponse as AssetHttpResponse, HttpResponseBuilder,
// };
// use ic_nervous_system_common::{serve_logs, serve_logs_v2, serve_metrics};

use ic_eth_rpc::*;

#[ic_cdk_macros::query]
#[candid_method(query)]
pub fn verify_signature(eth_address: Vec<u8>, message: Vec<u8>, signature: Vec<u8>) -> bool {
do_verify_signature(&eth_address, message, signature)
}

// TODO: logging for eth_* methods

#[update]
#[candid_method]
async fn eth_gas_price(source: Source) -> Result<u128> {
// do_http_request(source.resolve()?, "eth_gasPrice", (), 256).await
Ok(get_provider(source.resolve()?, 256)
.get_gas_price()
.await
.unwrap()
.try_into()
.unwrap())
}

#[update]
#[candid_method]
async fn eth_fee_history(
source: Source,
block_count: u128,
last_block: candid_types::BlockNumber,
reward_percentiles: Vec<f64>,
max_response_bytes: u64,
) -> Result<candid_types::FeeHistory> {
do_http_request(
source.resolve()?,
"eth_feeHistory",
(
block_count,
/* serialize */ (&last_block),
/* serialize */ (&reward_percentiles),
),
max_response_bytes,
)
.await
}

#[update]
#[candid_method]
async fn request(
source: Source,
json_rpc_payload: String,
max_response_bytes: u64,
) -> Result<Vec<u8>, EthRpcError> {
do_http_request(source.resolve()?, &json_rpc_payload, max_response_bytes).await
) -> Result<Vec<u8>> {
do_http_request_str(source.resolve()?, &json_rpc_payload, max_response_bytes).await
}

#[query]
#[candid_method(query)]
fn request_cost(
source: Source,
json_rpc_payload: String,
max_response_bytes: u64,
) -> Result<u128, EthRpcError> {
fn request_cost(source: Source, json_rpc_payload: String, max_response_bytes: u64) -> Result<u128> {
Ok(get_request_cost(
&source.resolve().unwrap(),
&json_rpc_payload,
Expand Down
45 changes: 45 additions & 0 deletions src/rpc.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use crate::*;
use async_trait::async_trait;
use ethers_providers::{JsonRpcClient, ProviderError};
use serde::{de::DeserializeOwned, Serialize};
use std::fmt::Debug;

pub fn get_provider(
source: ResolvedSource,
max_response_bytes: u64,
) -> ethers_providers::Provider<HttpOutcallClient> {
ethers_providers::Provider::new(HttpOutcallClient::new(source, max_response_bytes))
}

#[derive(Debug)]
pub struct HttpOutcallClient {
pub source: ResolvedSource,
pub max_response_bytes: u64,
}

impl HttpOutcallClient {
pub fn new(source: ResolvedSource, max_response_bytes: u64) -> Self {
Self {
source,
max_response_bytes,
}
}
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl JsonRpcClient for HttpOutcallClient {
type Error = ProviderError;

async fn request<T, R>(&self, method: &str, params: T) -> Result<R, Self::Error>
where
T: Debug + Serialize + Send + Sync,
R: DeserializeOwned,
{
Ok(
do_http_request(self.source.clone(), method, params, self.max_response_bytes)
.await
.unwrap(), // TODO: error handling
)
}
}
70 changes: 69 additions & 1 deletion src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ use std::collections::{HashMap, HashSet};
use crate::constants::STRING_STORABLE_MAX_SIZE;
use crate::PROVIDERS;

pub type Result<T, E = EthRpcError> = std::result::Result<T, E>;

#[derive(Clone, Debug, CandidType, Deserialize)]
pub enum Source {
Url(String),
Expand All @@ -15,7 +17,7 @@ pub enum Source {
}

impl Source {
pub fn resolve(self) -> Result<ResolvedSource, EthRpcError> {
pub fn resolve(self) -> Result<ResolvedSource> {
Ok(match self {
Source::Url(name) => ResolvedSource::Url(name),
Source::Provider(id) => ResolvedSource::Provider({
Expand All @@ -42,6 +44,7 @@ impl Source {
}
}

#[derive(Clone, Debug)]
pub enum ResolvedSource {
Url(String),
Provider(Provider),
Expand Down Expand Up @@ -192,7 +195,72 @@ pub enum EthRpcError {
ServiceUrlHostNotAllowed,
ProviderNotFound,
ProviderNotActive,
SerializeError,
HttpRequestError { code: u32, message: String },
}

pub type AllowlistSet = HashSet<&'static &'static str>;

pub mod candid_types {
use candid::CandidType;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, CandidType)]
pub enum BlockNumber {
Latest,
Finalized,
Safe,
Earliest,
Pending,
Number(u64),
}

impl Into<ic_eth::core::types::BlockNumber> for BlockNumber {
fn into(self) -> ic_eth::core::types::BlockNumber {
use ic_eth::core::types::BlockNumber::*;
match self {
Self::Latest => Latest,
Self::Finalized => Finalized,
Self::Safe => Safe,
Self::Earliest => Earliest,
Self::Pending => Pending,
Self::Number(n) => Number(n.into()),
}
}
}

#[derive(Deserialize, Serialize, Debug, Clone, CandidType)]
#[serde(rename_all = "camelCase")]
pub struct FeeHistory {
pub base_fee_per_gas: Vec<u128>,
pub gas_used_ratio: Vec<f64>,
/// oldestBlock is returned as an unsigned integer up to geth v1.10.6. From
/// geth v1.10.7, this has been updated to return in the hex encoded form.
/// The custom deserializer allows backward compatibility for those clients
/// not running v1.10.7 yet.
pub oldest_block: u128,
/// An (optional) array of effective priority fee per gas data points from a single block. All
/// zeroes are returned if the block is empty.
#[serde(default)]
pub reward: Vec<Vec<u128>>,
}

impl Into<ic_eth::core::types::FeeHistory> for FeeHistory {
fn into(self) -> ic_eth::core::types::FeeHistory {
ic_eth::core::types::FeeHistory {
base_fee_per_gas: self
.base_fee_per_gas
.into_iter()
.map(|x| x.into())
.collect(),
gas_used_ratio: self.gas_used_ratio,
oldest_block: self.oldest_block.into(),
reward: self
.reward
.into_iter()
.map(|x| x.into_iter().map(|x| x.into()).collect())
.collect(),
}
}
}
}