Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions Cargo.lock

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

10 changes: 10 additions & 0 deletions candid/evm_rpc.did
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,11 @@ type ValidationError = variant {
Custom : text;
InvalidHex : text;
};
type JsonRequestResult = variant { Ok : text; Err : RpcError };
type MultiJsonRequestResult = variant {
Consistent : JsonRequestResult;
Inconsistent : vec record { RpcService; JsonRequestResult };
};
service : (InstallArgs) -> {
eth_feeHistory : (RpcServices, opt RpcConfig, FeeHistoryArgs) -> (MultiFeeHistoryResult);
eth_getBlockByNumber : (RpcServices, opt RpcConfig, BlockTag) -> (MultiGetBlockByNumberResult);
Expand All @@ -302,6 +307,11 @@ service : (InstallArgs) -> {
eth_getTransactionReceipt : (RpcServices, opt RpcConfig, hash : text) -> (MultiGetTransactionReceiptResult);
eth_sendRawTransaction : (RpcServices, opt RpcConfig, rawSignedTransactionHex : text) -> (MultiSendRawTransactionResult);
eth_call : (RpcServices, opt RpcConfig, CallArgs) -> (MultiCallResult);
// This endpoint should be used instead of the `request` endpoint as it allows aggregating the responses from
// multiple providers. It also takes parameters consistent with `eth_*` endpoints, i.e. an `RpcServices` instead
// of an `RpcService`.
multi_request : (RpcServices, opt RpcConfig, json : text) -> (MultiJsonRequestResult);
// DEPRECATED: Use `multi_request` instead.
request : (RpcService, json : text, maxResponseBytes : nat64) -> (RequestResult);
requestCost : (RpcService, json : text, maxResponseBytes : nat64) -> (RequestCostResult) query;

Expand Down
1 change: 1 addition & 0 deletions evm_rpc_client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ evm_rpc_types = { path = "../evm_rpc_types", features = ["alloy"] }
ic-cdk = { workspace = true }
ic-error-types = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
strum = { workspace = true }

[dev-dependencies]
Expand Down
49 changes: 47 additions & 2 deletions evm_rpc_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,8 @@ use crate::request::{
CallRequest, CallRequestBuilder, FeeHistoryRequest, FeeHistoryRequestBuilder,
GetBlockByNumberRequest, GetBlockByNumberRequestBuilder, GetTransactionCountRequest,
GetTransactionCountRequestBuilder, GetTransactionReceiptRequest,
GetTransactionReceiptRequestBuilder, Request, RequestBuilder, SendRawTransactionRequest,
SendRawTransactionRequestBuilder,
GetTransactionReceiptRequestBuilder, JsonRequest, JsonRequestBuilder, Request, RequestBuilder,
SendRawTransactionRequest, SendRawTransactionRequestBuilder,
};
use candid::{CandidType, Principal};
use evm_rpc_types::{
Expand Down Expand Up @@ -611,6 +611,51 @@ impl<R> EvmRpcClient<R> {
)
}

/// Call `multi_request` on the EVM RPC canister.
///
/// Note: The EVM RPC canister overrides the `id` field in the JSON-RPC
/// request payload with the next value from its internal sequential counter.
///
/// # Examples
///
/// ```rust
/// use alloy_primitives::U256;
/// use evm_rpc_client::EvmRpcClient;
/// use serde_json::json;
/// use std::str::FromStr;
///
/// # use evm_rpc_types::MultiRpcResult;
/// # #[tokio::main]
/// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
/// let client = EvmRpcClient::builder_for_ic()
/// # .with_default_stub_response(MultiRpcResult::Consistent(Ok("0x24604d0d".to_string())))
/// .build();
///
/// let result = client
/// .multi_request(json!({
/// "jsonrpc": "2.0",
/// // This value is overwritten by the EVM RPC canister
/// "id": 73,
/// "method": "eth_gasPrice",
/// }))
/// .send()
/// .await
/// .expect_consistent()
/// .map(|result| U256::from_str(&result).unwrap())
/// .unwrap();
///
/// assert_eq!(result, U256::from(0x24604d0d_u64));
/// # Ok(())
/// # }
/// ```
pub fn multi_request(&self, params: serde_json::Value) -> JsonRequestBuilder<R> {
RequestBuilder::new(
self.clone(),
JsonRequest::try_from(params).expect("Client error: invalid JSON request"),
10_000_000_000,
)
}

/// Call `eth_sendRawTransaction` on the EVM RPC canister.
///
/// # Examples
Expand Down
34 changes: 34 additions & 0 deletions evm_rpc_client/src/request/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -274,6 +274,37 @@ pub type GetTransactionReceiptRequestBuilder<R> = RequestBuilder<
MultiRpcResult<Option<alloy_rpc_types::TransactionReceipt>>,
>;

#[derive(Debug, Clone)]
pub struct JsonRequest(String);

impl TryFrom<serde_json::Value> for JsonRequest {
type Error = String;

fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
serde_json::to_string(&value)
.map(JsonRequest)
.map_err(|e| e.to_string())
}
}

impl EvmRpcRequest for JsonRequest {
type Config = RpcConfig;
type Params = String;
type CandidOutput = MultiRpcResult<String>;
type Output = MultiRpcResult<String>;

fn endpoint(&self) -> EvmRpcEndpoint {
EvmRpcEndpoint::MultiRequest
}

fn params(self) -> Self::Params {
self.0
}
}

pub type JsonRequestBuilder<R> =
RequestBuilder<R, RpcConfig, String, MultiRpcResult<String>, MultiRpcResult<String>>;

#[derive(Debug, Clone)]
pub struct SendRawTransactionRequest(Hex);

Expand Down Expand Up @@ -339,6 +370,8 @@ pub enum EvmRpcEndpoint {
GetTransactionCount,
/// `eth_getTransactionReceipt` endpoint.
GetTransactionReceipt,
/// `multi_request` endpoint.
MultiRequest,
/// `eth_sendRawTransaction` endpoint.
SendRawTransaction,
}
Expand All @@ -353,6 +386,7 @@ impl EvmRpcEndpoint {
Self::GetLogs => "eth_getLogs",
Self::GetTransactionCount => "eth_getTransactionCount",
Self::GetTransactionReceipt => "eth_getTransactionReceipt",
Self::MultiRequest => "multi_request",
Self::SendRawTransaction => "eth_sendRawTransaction",
}
}
Expand Down
36 changes: 33 additions & 3 deletions src/candid_rpc/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,22 @@ mod cketh_conversion;
mod tests;

use crate::rpc_client::{EthRpcClient, ReducedResult};
use crate::types::MetricRpcMethod;
use crate::{
add_metric_entry,
providers::resolve_rpc_service,
types::{MetricRpcHost, ResolvedRpcService, RpcMethod},
};
use candid::Nat;
use canhttp::http::json::JsonRpcRequest;
use canhttp::multi::{ReductionError, Timestamp};
use ethers_core::{types::Transaction, utils::rlp};
use evm_rpc_types::{Hex, Hex32, MultiRpcResult, Nat256, RpcResult, ValidationError};
use evm_rpc_types::{Hex, Hex32, MultiRpcResult, Nat256, RpcError, RpcResult, ValidationError};

fn process_result<T>(method: RpcMethod, result: ReducedResult<T>) -> MultiRpcResult<T> {
fn process_result<T>(
method: impl Into<MetricRpcMethod> + Clone,
result: ReducedResult<T>,
) -> MultiRpcResult<T> {
match result {
Ok(value) => MultiRpcResult::Consistent(Ok(value)),
Err(err) => match err {
Expand All @@ -27,7 +32,7 @@ fn process_result<T>(method: RpcMethod, result: ReducedResult<T>) -> MultiRpcRes
add_metric_entry!(
inconsistent_responses,
(
method.into(),
method.clone().into(),
MetricRpcHost(
provider
.hostname()
Expand Down Expand Up @@ -172,6 +177,31 @@ impl CandidRpcClient {
)
.map(from_data)
}

pub async fn multi_request(&self, json_rpc_payload: String) -> MultiRpcResult<String> {
let request: JsonRpcRequest<serde_json::Value> =
match serde_json::from_str(&json_rpc_payload) {
Ok(req) => req,
Err(e) => {
return MultiRpcResult::Consistent(Err(RpcError::ValidationError(
ValidationError::Custom(format!("Invalid JSON RPC request: {e}")),
)))
}
};
process_result(
MetricRpcMethod {
method: request.method().to_string(),
is_manual_request: true,
},
self.client
.multi_request(
RpcMethod::Custom(request.method().to_string()),
request.params(),
)
.await,
)
.map(String::from)
}
}

fn get_transaction_hash(raw_signed_transaction_hex: &Hex) -> Option<Hex32> {
Expand Down
12 changes: 5 additions & 7 deletions src/candid_rpc/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,13 @@ fn test_process_result_mapping() {
use evm_rpc_types::{EthMainnetService, RpcService};
type ReductionError = canhttp::multi::ReductionError<RpcService, u32, RpcError>;

let method = RpcMethod::EthGetTransactionCount;

assert_eq!(
process_result(method, Ok(5)),
process_result(RpcMethod::EthGetTransactionCount, Ok(5)),
MultiRpcResult::Consistent(Ok(5))
);
assert_eq!(
process_result(
method,
RpcMethod::EthGetTransactionCount,
Err(ReductionError::ConsistentError(RpcError::ProviderError(
ProviderError::MissingRequiredProvider
)))
Expand All @@ -28,14 +26,14 @@ fn test_process_result_mapping() {
);
assert_eq!(
process_result(
method,
RpcMethod::EthGetTransactionCount,
Err(ReductionError::InconsistentResults(MultiResults::default()))
),
MultiRpcResult::Inconsistent(vec![])
);
assert_eq!(
process_result(
method,
RpcMethod::EthGetTransactionCount,
Err(ReductionError::InconsistentResults(
MultiResults::from_non_empty_iter(vec![(
RpcService::EthMainnet(EthMainnetService::Ankr),
Expand All @@ -50,7 +48,7 @@ fn test_process_result_mapping() {
);
assert_eq!(
process_result(
method,
RpcMethod::EthGetTransactionCount,
Err(ReductionError::InconsistentResults(
MultiResults::from_non_empty_iter(vec![
(RpcService::EthMainnet(EthMainnetService::Ankr), Ok(5)),
Expand Down
13 changes: 10 additions & 3 deletions src/http.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use ic_cdk::api::management_canister::http_request::{
};
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::cmp::PartialEq;
use std::fmt::Debug;
use thiserror::Error;
use tower::layer::util::{Identity, Stack};
Expand Down Expand Up @@ -76,9 +77,15 @@ pub async fn json_rpc_request(
max_response_bytes: u64,
) -> RpcResult<HttpJsonRpcResponse<serde_json::Value>> {
let request = json_rpc_request_arg(service, json_rpc_payload, max_response_bytes)?;
http_client(MetricRpcMethod("request".to_string()), false)
.call(request)
.await
http_client(
MetricRpcMethod {
method: "request".to_string(),
is_manual_request: true,
},
false,
)
.call(request)
.await
}

pub fn http_client<I, O>(
Expand Down
41 changes: 27 additions & 14 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ use evm_rpc::{
providers::{find_provider, resolve_rpc_service, PROVIDERS, SERVICE_PROVIDER_MAP},
types::{OverrideProvider, Provider, ProviderId, RpcAccess, RpcAuth},
};
use evm_rpc_types::{Hex32, HttpOutcallError, MultiRpcResult, RpcConfig, RpcResult};
use evm_rpc_types::{Hex32, HttpOutcallError, MultiRpcResult, RpcConfig, RpcResult, RpcServices};
use ic_canister_log::log;
use ic_cdk::{
api::{
Expand Down Expand Up @@ -46,7 +46,7 @@ pub fn require_api_key_principal_or_controller() -> Result<(), String> {
#[update(name = "eth_getLogs")]
#[candid_method(rename = "eth_getLogs")]
pub async fn eth_get_logs(
source: evm_rpc_types::RpcServices,
source: RpcServices,
config: Option<evm_rpc_types::GetLogsRpcConfig>,
args: evm_rpc_types::GetLogsArgs,
) -> MultiRpcResult<Vec<evm_rpc_types::LogEntry>> {
Expand All @@ -61,8 +61,8 @@ pub async fn eth_get_logs(
#[update(name = "eth_getBlockByNumber")]
#[candid_method(rename = "eth_getBlockByNumber")]
pub async fn eth_get_block_by_number(
source: evm_rpc_types::RpcServices,
config: Option<evm_rpc_types::RpcConfig>,
source: RpcServices,
config: Option<RpcConfig>,
block: evm_rpc_types::BlockTag,
) -> MultiRpcResult<evm_rpc_types::Block> {
match CandidRpcClient::new(source, config, now()) {
Expand All @@ -74,8 +74,8 @@ pub async fn eth_get_block_by_number(
#[update(name = "eth_getTransactionReceipt")]
#[candid_method(rename = "eth_getTransactionReceipt")]
pub async fn eth_get_transaction_receipt(
source: evm_rpc_types::RpcServices,
config: Option<evm_rpc_types::RpcConfig>,
source: RpcServices,
config: Option<RpcConfig>,
tx_hash: Hex32,
) -> MultiRpcResult<Option<evm_rpc_types::TransactionReceipt>> {
match CandidRpcClient::new(source, config, now()) {
Expand All @@ -87,8 +87,8 @@ pub async fn eth_get_transaction_receipt(
#[update(name = "eth_getTransactionCount")]
#[candid_method(rename = "eth_getTransactionCount")]
pub async fn eth_get_transaction_count(
source: evm_rpc_types::RpcServices,
config: Option<evm_rpc_types::RpcConfig>,
source: RpcServices,
config: Option<RpcConfig>,
args: evm_rpc_types::GetTransactionCountArgs,
) -> MultiRpcResult<evm_rpc_types::Nat256> {
match CandidRpcClient::new(source, config, now()) {
Expand All @@ -100,8 +100,8 @@ pub async fn eth_get_transaction_count(
#[update(name = "eth_feeHistory")]
#[candid_method(rename = "eth_feeHistory")]
pub async fn eth_fee_history(
source: evm_rpc_types::RpcServices,
config: Option<evm_rpc_types::RpcConfig>,
source: RpcServices,
config: Option<RpcConfig>,
args: evm_rpc_types::FeeHistoryArgs,
) -> MultiRpcResult<evm_rpc_types::FeeHistory> {
match CandidRpcClient::new(source, config, now()) {
Expand All @@ -113,8 +113,8 @@ pub async fn eth_fee_history(
#[update(name = "eth_sendRawTransaction")]
#[candid_method(rename = "eth_sendRawTransaction")]
pub async fn eth_send_raw_transaction(
source: evm_rpc_types::RpcServices,
config: Option<evm_rpc_types::RpcConfig>,
source: RpcServices,
config: Option<RpcConfig>,
raw_signed_transaction_hex: evm_rpc_types::Hex,
) -> MultiRpcResult<evm_rpc_types::SendRawTransactionStatus> {
match CandidRpcClient::new(source, config, now()) {
Expand All @@ -130,8 +130,8 @@ pub async fn eth_send_raw_transaction(
#[update(name = "eth_call")]
#[candid_method(rename = "eth_call")]
pub async fn eth_call(
source: evm_rpc_types::RpcServices,
config: Option<evm_rpc_types::RpcConfig>,
source: RpcServices,
config: Option<RpcConfig>,
args: evm_rpc_types::CallArgs,
) -> MultiRpcResult<evm_rpc_types::Hex> {
match CandidRpcClient::new(source, config, now()) {
Expand All @@ -140,6 +140,19 @@ pub async fn eth_call(
}
}

#[update(name = "multi_request")]
#[candid_method(rename = "multi_request")]
pub async fn multi_request(
source: RpcServices,
config: Option<RpcConfig>,
args: String,
) -> MultiRpcResult<String> {
match CandidRpcClient::new(source, config, now()) {
Ok(source) => source.multi_request(args).await,
Err(err) => Err(err).into(),
}
}

#[update]
#[candid_method]
async fn request(
Expand Down
Loading