use crate::Result;
use alloy_primitives::{Address, U256};
use serde::Deserialize;
use std::time::Duration;
pub struct RpcClient {
client: reqwest::Client,
url: String,
}
impl RpcClient {
pub fn new(url: &str) -> Self {
Self {
client: reqwest::Client::builder()
.timeout(Duration::from_secs(30))
.build()
.expect("Failed to build HTTP client"),
url: url.to_string(),
}
}
async fn request<T: for<'de> Deserialize<'de>>(&self, method: &str, params: serde_json::Value) -> Result<T> {
let request = serde_json::json!({
"jsonrpc": "2.0",
"method": method,
"params": params,
"id": 1
});
let response = self.client
.post(&self.url)
.json(&request)
.send()
.await?
.json::<RpcResponse<T>>()
.await?;
if let Some(error) = response.error {
Err(crate::Error::Rpc(error.message))
} else {
response.result.ok_or_else(|| crate::Error::Rpc("null result".to_string()))
}
}
pub async fn get_chain_id(&self) -> Result<u64> {
let id: String = self.request("eth_chainId", serde_json::Value::Null).await?;
Ok(u64::from_str_radix(&id[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
}
pub async fn get_block_number(&self) -> Result<u64> {
let block: String = self.request("eth_blockNumber", serde_json::Value::Null).await?;
Ok(u64::from_str_radix(&block[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
}
pub async fn get_balance(&self, address: Address) -> Result<U256> {
let balance: String = self.request("eth_getBalance", serde_json::json!([address.to_string(), "latest"])).await?;
Ok(U256::from_str_radix(&balance[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
}
pub async fn get_erc20_balance(&self, token: Address, owner: Address) -> Result<U256> {
let owner_hex = owner.to_string();
let owner_bytes = &owner_hex[2..];
let data = format!("0x70a082310000000000000000000000000000000000000000000000000000000000000000{}", owner_bytes);
let result: String = self.request("eth_call", serde_json::json!([
{ "to": token.to_string(), "data": data },
"latest"
])).await?;
Ok(U256::from_str_radix(&result[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
}
pub async fn get_transaction_count(&self, address: Address, block: &str) -> Result<u64> {
let nonce: String = self.request("eth_getTransactionCount", serde_json::json!([address.to_string(), block])).await?;
Ok(u64::from_str_radix(&nonce[2..], 16).map_err(|e| crate::Error::InvalidHex(e.to_string()))?)
}
pub async fn send_raw_transaction(&self, tx: String) -> Result<String> {
self.request("eth_sendRawTransaction", serde_json::json!([tx])).await
}
pub async fn get_transaction_receipt(&self, tx_hash: String) -> Result<Option<TransactionReceipt>> {
self.request("eth_getTransactionReceipt", serde_json::json!([tx_hash])).await
}
pub async fn get_transaction_by_hash(&self, tx_hash: String) -> Result<Option<Transaction>> {
self.request("eth_getTransactionByHash", serde_json::json!([tx_hash])).await
}
pub async fn get_code(&self, address: Address) -> Result<String> {
self.request("eth_getCode", serde_json::json!([address.to_string(), "latest"])).await
}
pub async fn wait_for_receipt(&self, tx_hash: String, timeout_ms: u64) -> Result<TransactionReceipt> {
let start = std::time::Instant::now();
let poll_interval = std::time::Duration::from_millis(500);
while start.elapsed().as_millis() < timeout_ms as u128 {
if let Some(receipt) = self.get_transaction_receipt(tx_hash.clone()).await? {
return Ok(receipt);
}
tokio::time::sleep(poll_interval).await;
}
Err(crate::Error::Rpc("Transaction receipt timeout".to_string()))
}
}
#[derive(Debug, Deserialize)]
struct RpcResponse<T> {
#[serde(rename = "result")]
result: Option<T>,
#[serde(rename = "error")]
error: Option<RpcError>,
}
#[derive(Debug, Deserialize)]
struct RpcError {
message: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct TransactionReceipt {
pub transaction_hash: String,
pub block_number: String,
pub status: String,
pub gas_used: String,
}
#[derive(Debug, Deserialize, Clone)]
pub struct Transaction {
pub hash: String,
pub from: String,
pub to: Option<String>,
pub value: String,
pub input: String,
pub gas: String,
pub gas_price: Option<String>,
pub nonce: String,
#[serde(rename = "type")]
pub tx_type: String,
}
pub fn create_rpc_client(url: &str) -> RpcClient {
RpcClient::new(url)
}