use alloy_primitives::{Address, B256, Bytes, U64, U256};
use async_trait::async_trait;
use serde::de::DeserializeOwned;
use serde_json::{Value, json};
use crate::chains::{self, ChainInfo};
use crate::error::{Error, Result};
use crate::provider::ChainProvider;
use crate::scan::LogScan;
use crate::transport::Transport;
use crate::types::{Block, BlockId, CallRequest, Filter, Log, Receipt, Transaction};
#[derive(Debug, Clone)]
pub struct EvmClient<T> {
transport: T,
chain: ChainInfo,
}
#[cfg(feature = "http")]
impl EvmClient<crate::transport::HttpTransport> {
pub async fn connect(url_or_key: impl AsRef<str>) -> Result<Self> {
let rpc = chains::resolve_rpc(url_or_key.as_ref())?;
let transport = crate::transport::HttpTransport::new(rpc.url);
match rpc.chain.and_then(|c| c.evm_chain_id) {
Some(expected) => Self::detect_expecting(transport, expected).await,
None if rpc.chain.is_some() => Err(Error::Unsupported(format!(
"`{}` is not an EVM chain; use its adapter (e.g. adapters::solana::connect)",
url_or_key.as_ref()
))),
None => Self::detect(transport).await,
}
}
}
impl<T: Transport> EvmClient<T> {
pub fn new(transport: T, chain: ChainInfo) -> Self {
Self { transport, chain }
}
pub async fn detect(transport: T) -> Result<Self> {
let id: U64 = decode(
"eth_chainId",
transport.request("eth_chainId", json!([])).await?,
)?;
let id = id.to::<u64>();
let chain = chains::by_id(id).unwrap_or_else(|| ChainInfo::evm_unknown(id));
Ok(Self { transport, chain })
}
pub async fn detect_expecting(transport: T, expected_chain_id: u64) -> Result<Self> {
let client = Self::detect(transport).await?;
match client.chain.evm_chain_id {
Some(id) if id == expected_chain_id => Ok(client),
actual => Err(Error::ChainMismatch {
expected: expected_chain_id,
actual: actual.unwrap_or_default(),
}),
}
}
pub fn transport(&self) -> &T {
&self.transport
}
pub fn chain_info(&self) -> &ChainInfo {
&self.chain
}
pub async fn request<R: DeserializeOwned>(&self, method: &str, params: Value) -> Result<R> {
decode(method, self.transport.request(method, params).await?)
}
pub async fn chain_id(&self) -> Result<u64> {
Ok(self.request::<U64>("eth_chainId", json!([])).await?.to())
}
pub async fn block_number(&self) -> Result<u64> {
Ok(self
.request::<U64>("eth_blockNumber", json!([]))
.await?
.to())
}
pub async fn gas_price(&self) -> Result<U256> {
self.request("eth_gasPrice", json!([])).await
}
pub async fn get_balance(&self, address: Address, at: BlockId) -> Result<U256> {
self.request("eth_getBalance", json!([address, at.to_param()]))
.await
}
pub async fn get_transaction_count(&self, address: Address, at: BlockId) -> Result<u64> {
Ok(self
.request::<U64>("eth_getTransactionCount", json!([address, at.to_param()]))
.await?
.to())
}
pub async fn get_code(&self, address: Address, at: BlockId) -> Result<Bytes> {
self.request("eth_getCode", json!([address, at.to_param()]))
.await
}
pub async fn get_storage_at(&self, address: Address, slot: U256, at: BlockId) -> Result<B256> {
self.request("eth_getStorageAt", json!([address, slot, at.to_param()]))
.await
}
pub async fn get_block(&self, at: BlockId, full: bool) -> Result<Option<Block>> {
self.request("eth_getBlockByNumber", json!([at.to_param(), full]))
.await
}
pub async fn get_block_by_hash(&self, hash: B256, full: bool) -> Result<Option<Block>> {
self.request("eth_getBlockByHash", json!([hash, full]))
.await
}
pub async fn get_transaction(&self, hash: B256) -> Result<Option<Transaction>> {
self.request("eth_getTransactionByHash", json!([hash]))
.await
}
pub async fn get_transaction_receipt(&self, hash: B256) -> Result<Option<Receipt>> {
self.request("eth_getTransactionReceipt", json!([hash]))
.await
}
pub async fn get_logs(&self, filter: &Filter) -> Result<Vec<Log>> {
self.request("eth_getLogs", json!([filter])).await
}
pub async fn get_logs_chunked(
&self,
filter: &Filter,
from: u64,
to: u64,
max_range: u64,
) -> Result<Vec<Log>> {
self.scan_logs(filter.clone())
.from_block(from)
.to_block(to)
.chunk(max_range)
.run()
.await
}
pub fn scan_logs(&self, filter: Filter) -> LogScan<'_, T> {
LogScan::new(self, filter)
}
pub async fn block_at(&self, unix_secs: u64) -> Result<u64> {
let head = self
.get_block(BlockId::Latest, false)
.await?
.ok_or_else(|| Error::Unsupported("the node did not return the latest block".into()))?;
let (head_n, head_ts): (u64, u64) = (head.number.to(), head.timestamp.to());
if unix_secs >= head_ts || head_n == 0 {
return Ok(head_n);
}
let sample = head_n.min(1_000);
let sample_ts = self.timestamp_of(head_n - sample).await?;
let secs_per_block = (head_ts.saturating_sub(sample_ts) as f64 / sample as f64).max(0.001);
let back = ((head_ts - unix_secs) as f64 / secs_per_block).ceil() as u64;
let guess = head_n.saturating_sub(back);
let (mut lo, mut hi);
let mut step = (back / 20).max(100);
if self.timestamp_of(guess).await? >= unix_secs {
hi = guess;
loop {
if hi == 0 {
return Ok(0);
}
let candidate = hi.saturating_sub(step);
if self.timestamp_of(candidate).await? < unix_secs {
lo = candidate;
break;
}
hi = candidate;
step = step.saturating_mul(2);
}
} else {
lo = guess;
loop {
let candidate = lo.saturating_add(step).min(head_n);
if candidate == head_n || self.timestamp_of(candidate).await? >= unix_secs {
hi = candidate;
break;
}
lo = candidate;
step = step.saturating_mul(2);
}
}
while hi - lo > 1 {
let mid = lo + (hi - lo) / 2;
if self.timestamp_of(mid).await? >= unix_secs {
hi = mid;
} else {
lo = mid;
}
}
Ok(hi)
}
async fn timestamp_of(&self, number: u64) -> Result<u64> {
match self.get_block(BlockId::Number(number), false).await? {
Some(block) => Ok(block.timestamp.to()),
None => Err(Error::Unsupported(format!(
"this RPC does not serve block {number}: it is probably not an archive node \
and only keeps recent history. Pick a more recent date, or use an archive RPC \
(e.g. via EVMIFY_RPC_<KEY>)"
))),
}
}
pub async fn blocks_in(&self, duration: std::time::Duration) -> Result<u64> {
let missing = || Error::Unsupported("the node did not return the block".into());
let head = self
.get_block(BlockId::Latest, false)
.await?
.ok_or_else(missing)?;
let head_number: u64 = head.number.to();
let sample = head_number.min(1_000);
if sample == 0 {
return Ok(0);
}
let old = self
.get_block(BlockId::Number(head_number - sample), false)
.await?
.ok_or_else(missing)?;
let elapsed = head
.timestamp
.to::<u64>()
.saturating_sub(old.timestamp.to::<u64>());
if elapsed == 0 {
return Err(Error::Unsupported(
"cannot measure block time: timestamps are equal".into(),
));
}
Ok((duration.as_secs_f64() * sample as f64 / elapsed as f64).ceil() as u64)
}
pub async fn call(&self, call: &CallRequest, at: BlockId) -> Result<Bytes> {
self.request("eth_call", json!([call, at.to_param()])).await
}
pub async fn estimate_gas(&self, call: &CallRequest) -> Result<U256> {
self.request("eth_estimateGas", json!([call])).await
}
pub async fn send_raw_transaction(&self, raw: Bytes) -> Result<B256> {
self.request("eth_sendRawTransaction", json!([raw])).await
}
}
fn decode<R: DeserializeOwned>(method: &str, v: Value) -> Result<R> {
serde_json::from_value(v).map_err(|e| Error::decode(method, e))
}
#[async_trait]
impl<T: Transport> ChainProvider for EvmClient<T> {
fn chain(&self) -> &ChainInfo {
&self.chain
}
async fn block_number(&self) -> Result<u64> {
EvmClient::block_number(self).await
}
async fn block(&self, at: BlockId, full: bool) -> Result<Option<Block>> {
self.get_block(at, full).await
}
async fn transaction(&self, hash: B256) -> Result<Option<Transaction>> {
self.get_transaction(hash).await
}
async fn receipt(&self, hash: B256) -> Result<Option<Receipt>> {
self.get_transaction_receipt(hash).await
}
async fn logs(&self, filter: &Filter) -> Result<Vec<Log>> {
self.get_logs(filter).await
}
}