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
71pub 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 headers: Vec::new(),
162 }
163}