use ethers_core::abi::{Contract, FunctionExt, Token};
use ic_cdk::api::management_canister::http_request::{
http_request, CanisterHttpRequestArgument, HttpHeader, HttpMethod, HttpResponse, TransformArgs,
TransformContext,
};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use crate::util::{from_hex, to_hex};
const HTTP_CYCLES: u128 = 100_000_000;
const MAX_RESPONSE_BYTES: u64 = 2048;
#[derive(Clone, Debug, Serialize, Deserialize)]
struct JsonRpcRequest {
id: u64,
jsonrpc: String,
method: String,
params: (EthCallParams, String),
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct EthCallParams {
to: String,
data: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct JsonRpcResult {
result: Option<String>,
error: Option<JsonRpcError>,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
struct JsonRpcError {
code: isize,
message: String,
}
#[macro_export]
macro_rules! include_abi {
($file:expr $(,)?) => {{
match serde_json::from_str::<ethers_core::abi::Contract>(include_str!($file)) {
Ok(contract) => contract,
Err(err) => panic!("Error loading ABI contract {:?}: {}", $file, err),
}
}};
}
fn next_id() -> u64 {
thread_local! {
static NEXT_ID: RefCell<u64> = RefCell::default();
}
NEXT_ID.with(|next_id| {
let mut next_id = next_id.borrow_mut();
let id = *next_id;
*next_id = next_id.wrapping_add(1);
id
})
}
fn get_rpc_endpoint(network: &str) -> &'static str {
match network {
"mainnet" | "ethereum" => "https://cloudflare-eth.com/v1/mainnet",
"goerli" => "https://ethereum-goerli.publicnode.com",
"sepolia" => "https://rpc.sepolia.org",
_ => panic!("Unsupported network: {}", network),
}
}
pub async fn call_contract(
network: &str,
contract_address: String,
abi: &Contract,
function_name: &str,
args: &[Token],
) -> Vec<Token> {
let f = match abi.functions_by_name(function_name).map(|v| &v[..]) {
Ok([f]) => f,
Ok(fs) => panic!(
"Found {} function overloads. Please pass one of the following: {}",
fs.len(),
fs.iter()
.map(|f| format!("{:?}", f.abi_signature()))
.collect::<Vec<_>>()
.join(", ")
),
Err(_) => abi
.functions()
.find(|f| function_name == f.abi_signature())
.expect("Function not found"),
};
let data = f
.encode_input(args)
.expect("Error while encoding input args");
let service_url = get_rpc_endpoint(network).to_string();
let json_rpc_payload = serde_json::to_string(&JsonRpcRequest {
id: next_id(),
jsonrpc: "2.0".to_string(),
method: "eth_call".to_string(),
params: (
EthCallParams {
to: contract_address,
data: to_hex(&data),
},
"latest".to_string(),
),
})
.expect("Error while encoding JSON-RPC request");
let parsed_url = url::Url::parse(&service_url).expect("Service URL parse error");
let host = parsed_url
.host_str()
.expect("Invalid service URL host")
.to_string();
let request_headers = vec![
HttpHeader {
name: "Content-Type".to_string(),
value: "application/json".to_string(),
},
HttpHeader {
name: "Host".to_string(),
value: host.to_string(),
},
];
let request = CanisterHttpRequestArgument {
url: service_url,
max_response_bytes: Some(MAX_RESPONSE_BYTES),
method: HttpMethod::POST,
headers: request_headers,
body: Some(json_rpc_payload.as_bytes().to_vec()),
transform: Some(TransformContext::from_name(
"icp_eth_transform_response".to_string(),
vec![],
)),
};
let result = match http_request(request, HTTP_CYCLES).await {
Ok((r,)) => r,
Err((r, m)) => panic!("{:?} {:?}", r, m),
};
let json: JsonRpcResult =
serde_json::from_str(std::str::from_utf8(&result.body).expect("utf8"))
.expect("JSON was not well-formatted");
if let Some(err) = json.error {
panic!("JSON-RPC error code {}: {}", err.code, err.message);
}
let result = from_hex(&json.result.expect("Unexpected JSON response")).unwrap();
f.decode_output(&result).expect("Error decoding output")
}
#[ic_cdk::query(name = "icp_eth_transform_response")]
pub fn transform(args: TransformArgs) -> HttpResponse {
HttpResponse {
status: args.response.status.clone(),
body: args.response.body,
headers: Vec::new(),
}
}