use crate::ui::spinner::Spinner;
use alloy_json_abi::JsonAbi;
use alloy_primitives::{Address, Bytes, U256};
use edb_common::types::{CallableAbiInfo, Code, EdbSolValue, SnapshotInfo, Trace};
use eyre::Result;
use jsonrpsee::{
core::client::ClientT,
http_client::{HttpClient, HttpClientBuilder},
};
use serde_json::Value;
use std::{
collections::HashMap,
sync::{Arc, RwLock},
time::Duration,
};
use tracing::{debug, error};
macro_rules! rpc_params {
() => {
Vec::<serde_json::Value>::new()
};
($($param:expr),+ $(,)?) => {
vec![
$(serde_json::to_value($param).unwrap_or(serde_json::Value::Null)),+
]
};
}
#[derive(Debug)]
pub struct RpcClient {
client: HttpClient,
server_url: String,
spinner: Arc<RwLock<Spinner>>,
}
impl RpcClient {
pub async fn new(server_url: &str) -> Result<Self> {
let client = HttpClientBuilder::default()
.request_timeout(Duration::from_secs(30))
.build(server_url)?;
debug!("Created RPC client for: {}", server_url);
Ok(Self {
client,
server_url: server_url.to_string(),
spinner: Arc::new(RwLock::new(Spinner::new(None, None))),
})
}
pub async fn test_connection(server_url: &str) -> Result<()> {
debug!("Testing connection to: {}", server_url);
let client = HttpClientBuilder::default()
.request_timeout(Duration::from_secs(5))
.build(server_url)?;
match client.request::<Value, _>("debug.getSnapshotCount", rpc_params!()).await {
Ok(_) => {
debug!("Connection test successful for: {}", server_url);
Ok(())
}
Err(e) => {
debug!("Connection test failed for {}: {}", server_url, e);
Err(e.into())
}
}
}
async fn request_with_spinner(
&self,
method: &str,
params: Vec<Value>,
operation_name: &str,
) -> Result<Value> {
self.start_loading(operation_name);
debug!("Making RPC request: {}", operation_name);
let result = match self.client.request(method, params).await {
Ok(result) => {
debug!("{} successful: {:?}", operation_name, result);
Ok(result)
}
Err(e) => {
error!("{} failed: {}", operation_name, e);
Err(e.into())
}
};
self.finish_loading();
result
}
pub fn server_url(&self) -> &str {
&self.server_url
}
pub fn spinner(&self) -> Arc<RwLock<Spinner>> {
Arc::clone(&self.spinner)
}
pub fn is_loading(&self) -> bool {
self.spinner.read().unwrap().is_loading()
}
pub fn spinner_display(&self) -> String {
self.spinner.read().unwrap().display_text()
}
fn start_loading(&self, operation: &str) {
self.spinner.write().unwrap().start_loading(operation);
debug!("Started loading spinner: {}", operation);
}
fn finish_loading(&self) {
self.spinner.write().unwrap().finish_loading();
debug!("Finished loading spinner");
}
pub fn tick(&self) {
self.spinner.write().unwrap().tick();
}
pub async fn health_check(&self) -> Result<Value> {
debug!("Checking server health");
let health_url = format!("{}/health", self.server_url.trim_end_matches('/'));
let response = reqwest::get(&health_url).await?;
let health_data: Value = response.json().await?;
debug!("Server health: {:?}", health_data);
Ok(health_data)
}
}
impl RpcClient {
pub async fn get_trace(&self) -> Result<Trace> {
let value = self
.request_with_spinner("edb_getTrace", rpc_params!(), "Fetching execution trace")
.await?;
serde_json::from_value(value).map_err(|e| eyre::eyre!("Failed to parse trace: {}", e))
}
pub async fn get_contract_abi(
&self,
address: Address,
recompiled: bool,
) -> Result<Option<JsonAbi>> {
let value = self
.request_with_spinner(
"edb_getContractABI",
rpc_params!(address, recompiled),
&format!("Fetching contract ABI for {address}"),
)
.await?;
serde_json::from_value(value)
.map_err(|e| eyre::eyre!("Failed to parse contract ABI: {}", e))
}
pub async fn get_callable_abi(&self, address: Address) -> Result<Vec<CallableAbiInfo>> {
let value = self
.request_with_spinner(
"edb_getCallableABI",
rpc_params!(address),
&format!("Fetching callable ABI for {address}"),
)
.await?;
serde_json::from_value(value)
.map_err(|e| eyre::eyre!("Failed to parse callable ABI: {}", e))
}
pub async fn get_constructor_args(&self, address: Address) -> Result<Option<Bytes>> {
let value = self
.request_with_spinner(
"edb_getConstructorArgs",
rpc_params!(address),
&format!("Fetching contract constructor arguments for {address}"),
)
.await?;
serde_json::from_value(value)
.map_err(|e| eyre::eyre!("Failed to parse contract constructor arguments: {}", e))
}
pub async fn get_snapshot_count(&self) -> Result<usize> {
let value = self
.request_with_spinner(
"edb_getSnapshotCount",
rpc_params!(),
"Getting total snapshot count",
)
.await?;
serde_json::from_value(value)
.map_err(|e| eyre::eyre!("Failed to parse snapshot count: {}", e))
}
pub async fn get_snapshot_info(&self, snapshot_id: usize) -> Result<SnapshotInfo> {
let value = self
.request_with_spinner(
"edb_getSnapshotInfo",
rpc_params!(snapshot_id),
&format!("Getting info for snapshot {snapshot_id}"),
)
.await?;
serde_json::from_value(value)
.map_err(|e| eyre::eyre!("Failed to parse snapshot info: {}", e))
}
pub async fn get_code(&self, snapshot_id: usize) -> Result<Code> {
let value = self
.request_with_spinner(
"edb_getCode",
rpc_params!(snapshot_id),
&format!("Getting code for snapshot {snapshot_id}"),
)
.await?;
serde_json::from_value(value).map_err(|e| eyre::eyre!("Failed to parse code: {}", e))
}
pub async fn get_next_call(&self, snapshot_id: usize) -> Result<usize> {
let value = self
.request_with_spinner(
"edb_getNextCall",
rpc_params!(snapshot_id),
&format!("Getting next call for snapshot {snapshot_id}"),
)
.await?;
serde_json::from_value(value).map_err(|e| eyre::eyre!("Failed to parse next call: {}", e))
}
pub async fn get_prev_call(&self, snapshot_id: usize) -> Result<usize> {
let value = self
.request_with_spinner(
"edb_getPrevCall",
rpc_params!(snapshot_id),
&format!("Getting prev call for snapshot {snapshot_id}"),
)
.await?;
serde_json::from_value(value).map_err(|e| eyre::eyre!("Failed to parse prev call: {}", e))
}
pub async fn get_storage(&self, snapshot_id: usize, slot: U256) -> Result<U256> {
let value = self
.request_with_spinner(
"edb_getStorage",
rpc_params!(snapshot_id, slot),
&format!("Getting storage for snapshot {snapshot_id} at slot {slot}"),
)
.await?;
serde_json::from_value(value)
.map_err(|e| eyre::eyre!("Failed to parse storage value: {}", e))
}
pub async fn get_storage_diff(
&self,
snapshot_id: usize,
) -> Result<HashMap<U256, (U256, U256)>> {
let value = self
.request_with_spinner(
"edb_getStorageDiff",
rpc_params!(snapshot_id),
&format!("Getting storage diff for snapshot {snapshot_id}"),
)
.await?;
serde_json::from_value(value)
.map_err(|e| eyre::eyre!("Failed to parse storage diff: {}", e))
}
pub async fn eval_on_snapshot(
&self,
snapshot_id: usize,
expr: &str,
) -> Result<core::result::Result<EdbSolValue, String>> {
let value = self
.request_with_spinner(
"edb_evalOnSnapshot",
rpc_params!(snapshot_id, expr),
&format!("Evaluating expression on snapshot {snapshot_id}"),
)
.await?;
serde_json::from_value(value)
.map_err(|e| eyre::eyre!("Failed to parse evaluated value: {}", e))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[tokio::test]
async fn test_rpc_client_creation() {
let client = RpcClient::new("http://localhost:3000").await;
assert!(client.is_ok() || client.is_err()); }
#[test]
fn test_rpc_params_macro() {
let empty_params: Vec<Value> = rpc_params!();
let expected: Vec<Value> = vec![];
assert_eq!(empty_params, expected);
let single_param = rpc_params!(42);
assert_eq!(single_param, vec![json!(42)]);
let multi_params = rpc_params!("0x1234567890abcdef", true, 123);
assert_eq!(multi_params, vec![json!("0x1234567890abcdef"), json!(true), json!(123)]);
let address = "0xabcdef1234567890";
let recompiled = false;
let snapshot_id = 5;
let var_params = rpc_params!(address, recompiled, snapshot_id);
assert_eq!(var_params, vec![json!(address), json!(recompiled), json!(snapshot_id)]);
}
}