use reqwest::Client;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::rpc::{JsonRpcError, JsonRpcRequest, JsonRpcResponse};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SimulationResult {
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub gas_estimate: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub revert_reason: Option<String>,
}
pub async fn forward_to_upstream(
client: &Client,
rpc_url: &str,
request: &JsonRpcRequest,
) -> JsonRpcResponse {
let resp = match client.post(rpc_url).json(request).send().await {
Ok(resp) => resp,
Err(e) => {
return JsonRpcResponse::error(
request.id.clone(),
-32603,
format!("Failed to reach upstream RPC: {e}"),
);
}
};
let body = match resp.text().await {
Ok(body) => body,
Err(e) => {
return JsonRpcResponse::error(
request.id.clone(),
-32603,
format!("Failed to read upstream response body: {e}"),
);
}
};
match serde_json::from_str::<JsonRpcResponse>(&body) {
Ok(mut rpc_resp) => {
if rpc_resp.result.is_none() && rpc_resp.error.is_none() {
rpc_resp.result = Some(Value::Null);
}
rpc_resp
}
Err(_) => {
match serde_json::from_str::<Value>(&body) {
Ok(raw) => JsonRpcResponse {
jsonrpc: "2.0".into(),
result: Some(raw),
error: None,
id: request.id.clone(),
},
Err(e) => {
eprintln!(
" warn: upstream returned non-JSON for {}: {}",
request.method,
&body[..body.len().min(200)]
);
JsonRpcResponse::error(
request.id.clone(),
-32603,
format!("Upstream returned invalid JSON: {e}"),
)
}
}
}
}
}
pub async fn simulate_transaction(
client: &Client,
rpc_url: &str,
params: &Value,
) -> SimulationResult {
let tx_obj = if let Value::Array(arr) = params {
arr.first().cloned().unwrap_or(Value::Null)
} else {
params.clone()
};
let call_params = Value::Array(vec![tx_obj.clone(), Value::String("latest".into())]);
let gas_params = Value::Array(vec![tx_obj]);
let call_req = JsonRpcRequest {
jsonrpc: "2.0".into(),
method: "eth_call".into(),
params: call_params,
id: Value::Number(9990.into()),
};
let gas_req = JsonRpcRequest {
jsonrpc: "2.0".into(),
method: "eth_estimateGas".into(),
params: gas_params,
id: Value::Number(9991.into()),
};
let (call_resp, gas_resp) = tokio::join!(
forward_to_upstream(client, rpc_url, &call_req),
forward_to_upstream(client, rpc_url, &gas_req),
);
let mut result = SimulationResult {
success: true,
gas_estimate: None,
revert_reason: None,
};
if let Some(err) = &call_resp.error {
result.success = false;
result.revert_reason = Some(decode_revert_reason(err));
}
if let Some(ref res) = gas_resp.result {
if let Some(gas) = res.as_str() {
result.gas_estimate = Some(gas.to_string());
}
} else if let Some(err) = &gas_resp.error
&& result.success
{
result.success = false;
result.revert_reason = Some(decode_revert_reason(err));
}
result
}
fn decode_revert_reason(err: &JsonRpcError) -> String {
if let Some(ref data) = err.data {
let hex_data = if let Some(s) = data.as_str() {
Some(s.to_string())
} else if let Some(obj) = data.as_object() {
obj.get("data").and_then(|d| d.as_str()).map(String::from)
} else {
None
};
if let Some(hex) = hex_data
&& let Some(reason) = try_decode_revert_string(&hex)
{
return reason;
}
}
err.message.clone()
}
fn try_decode_revert_string(hex_data: &str) -> Option<String> {
let hex = hex_data.strip_prefix("0x")?;
if hex.len() < 136 {
return None;
}
if &hex[..8] != "08c379a0" {
return None;
}
let len_hex = hex[72..136].trim_start_matches('0');
let len = if len_hex.is_empty() {
0
} else {
usize::from_str_radix(len_hex, 16).ok()?
};
if len == 0 {
return Some(String::new());
}
let data_start = 136;
let data_end = data_start + len * 2;
if hex.len() < data_end {
return None;
}
let bytes: Vec<u8> = (0..len)
.filter_map(|i| {
u8::from_str_radix(&hex[data_start + i * 2..data_start + i * 2 + 2], 16).ok()
})
.collect();
Some(String::from_utf8_lossy(&bytes).to_string())
}
pub async fn fetch_chain_id(client: &Client, rpc_url: &str) -> Option<Value> {
let req = JsonRpcRequest {
jsonrpc: "2.0".into(),
method: "eth_chainId".into(),
params: Value::Array(vec![]),
id: Value::Number(1.into()),
};
let resp = forward_to_upstream(client, rpc_url, &req).await;
resp.result
}