icp_eth/
eth_rpc.rs

1use ethers_core::abi::{Contract, FunctionExt, Token};
2use ic_cdk::api::management_canister::http_request::{
3    http_request, CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformArgs,
4    TransformContext,
5};
6use serde::{Deserialize, Serialize};
7use std::cell::RefCell;
8
9use crate::util::{from_hex, to_hex};
10
11const HTTP_CYCLES: u128 = 100_000_000;
12const MAX_RESPONSE_BYTES: u64 = 2048;
13
14#[derive(Clone, Debug, Serialize, Deserialize)]
15struct JsonRpcRequest {
16    id: u64,
17    jsonrpc: String,
18    method: String,
19    params: (EthCallParams, String),
20}
21
22#[derive(Clone, Debug, Serialize, Deserialize)]
23struct EthCallParams {
24    to: String,
25    data: String,
26}
27
28#[derive(Clone, Debug, Serialize, Deserialize)]
29struct JsonRpcResult {
30    result: Option<String>,
31    error: Option<JsonRpcError>,
32}
33
34#[derive(Clone, Debug, Serialize, Deserialize)]
35struct JsonRpcError {
36    code: isize,
37    message: String,
38}
39
40#[macro_export]
41macro_rules! include_abi {
42    ($file:expr $(,)?) => {{
43        match serde_json::from_str::<ethers_core::abi::Contract>(include_str!($file)) {
44            Ok(contract) => contract,
45            Err(err) => panic!("Error loading ABI contract {:?}: {}", $file, err),
46        }
47    }};
48}
49
50fn next_id() -> u64 {
51    thread_local! {
52        static NEXT_ID: RefCell<u64> = RefCell::default();
53    }
54    NEXT_ID.with(|next_id| {
55        let mut next_id = next_id.borrow_mut();
56        let id = *next_id;
57        *next_id = next_id.wrapping_add(1);
58        id
59    })
60}
61
62fn get_rpc_endpoint(network: &str) -> &'static str {
63    match network {
64        "mainnet" | "ethereum" => "https://cloudflare-eth.com/v1/mainnet",
65        "goerli" => "https://ethereum-goerli.publicnode.com",
66        "sepolia" => "https://rpc.sepolia.org",
67        _ => panic!("Unsupported network: {}", network),
68    }
69}
70
71/// Call an Ethereum smart contract.
72pub async fn call_contract(
73    network: &str,
74    contract_address: String,
75    abi: &Contract,
76    function_name: &str,
77    args: &[Token],
78) -> Vec<Token> {
79    let f = match abi.functions_by_name(function_name).map(|v| &v[..]) {
80        Ok([f]) => f,
81        Ok(fs) => panic!(
82            "Found {} function overloads. Please pass one of the following: {}",
83            fs.len(),
84            fs.iter()
85                .map(|f| format!("{:?}", f.abi_signature()))
86                .collect::<Vec<_>>()
87                .join(", ")
88        ),
89        Err(_) => abi
90            .functions()
91            .find(|f| function_name == f.abi_signature())
92            .expect("Function not found"),
93    };
94    let data = f
95        .encode_input(args)
96        .expect("Error while encoding input args");
97    let service_url = get_rpc_endpoint(network).to_string();
98    let json_rpc_payload = serde_json::to_string(&JsonRpcRequest {
99        id: next_id(),
100        jsonrpc: "2.0".to_string(),
101        method: "eth_call".to_string(),
102        params: (
103            EthCallParams {
104                to: contract_address,
105                data: to_hex(&data),
106            },
107            "latest".to_string(),
108        ),
109    })
110    .expect("Error while encoding JSON-RPC request");
111
112    let parsed_url = url::Url::parse(&service_url).expect("Service URL parse error");
113    let host = parsed_url
114        .host_str()
115        .expect("Invalid service URL host")
116        .to_string();
117
118    let request_headers = vec![
119        HttpHeader {
120            name: "Content-Type".to_string(),
121            value: "application/json".to_string(),
122        },
123        HttpHeader {
124            name: "Host".to_string(),
125            value: host.to_string(),
126        },
127    ];
128    let request = CanisterHttpRequestArgument {
129        url: service_url,
130        max_response_bytes: Some(MAX_RESPONSE_BYTES),
131        method: HttpMethod::POST,
132        headers: request_headers,
133        body: Some(json_rpc_payload.as_bytes().to_vec()),
134        transform: Some(TransformContext::from_name(
135            "icp_eth_transform_response".to_string(),
136            vec![],
137        )),
138    };
139    let result = match http_request(request, HTTP_CYCLES).await {
140        Ok((r,)) => r,
141        Err((r, m)) => panic!("{:?} {:?}", r, m),
142    };
143
144    let json: JsonRpcResult =
145        serde_json::from_str(std::str::from_utf8(&result.body).expect("utf8"))
146            .expect("JSON was not well-formatted");
147    if let Some(err) = json.error {
148        panic!("JSON-RPC error code {}: {}", err.code, err.message);
149    }
150    let result = from_hex(&json.result.expect("Unexpected JSON response")).unwrap();
151    f.decode_output(&result).expect("Error decoding output")
152}
153
154#[ic_cdk::query(name = "icp_eth_transform_response")]
155pub fn transform(args: TransformArgs) -> HttpResponse {
156    HttpResponse {
157        status: args.response.status.clone(),
158        body: args.response.body,
159        // Strip headers as they contain the Date which is not necessarily the same
160        // and will prevent consensus on the result.
161        headers: Vec::new(),
162    }
163}