use std::sync::atomic::{AtomicI64, Ordering};
use std::time::Duration;
use serde::Deserialize;
use serde_json::{json, Value};
use crate::Error;
pub const DEFAULT_RPC_TIMEOUT: Duration = Duration::from_secs(15);
pub const DEFAULT_BLOCK_TAG: &str = "latest";
use crate::client::MAX_RESPONSE_BYTES;
#[derive(Debug, Clone)]
pub enum CallOutcome {
Ok(String),
Reverted(String),
}
#[derive(Debug, Deserialize)]
struct RpcResponse {
result: Option<Value>,
error: Option<RpcErrorBody>,
}
#[derive(Debug, Deserialize)]
struct RpcErrorBody {
code: Option<i64>,
message: Option<String>,
data: Option<Value>,
}
#[derive(Debug)]
pub struct RpcClient {
url: String,
http: reqwest::Client,
block: String,
next_id: AtomicI64,
}
impl RpcClient {
pub fn new(url: impl Into<String>, timeout: Duration) -> Result<Self, Error> {
let http = reqwest::Client::builder().timeout(timeout).build()?;
Ok(Self::with_http(url, http))
}
pub fn with_http(url: impl Into<String>, http: reqwest::Client) -> Self {
Self {
url: url.into(),
http,
block: DEFAULT_BLOCK_TAG.to_string(),
next_id: AtomicI64::new(1),
}
}
pub fn block(mut self, block: impl Into<String>) -> Self {
self.block = block.into();
self
}
pub fn url(&self) -> &str {
&self.url
}
pub fn block_tag(&self) -> &str {
&self.block
}
pub async fn request(&self, method: &str, params: Value) -> Result<Value, Error> {
let id = self.next_id.fetch_add(1, Ordering::Relaxed);
let body = json!({ "jsonrpc": "2.0", "id": id, "method": method, "params": params });
let response = self
.http
.post(&self.url)
.json(&body)
.send()
.await
.map_err(|source| Error::RpcTransport {
method: method.to_string(),
source,
})?;
let status = response.status();
let text = read_capped(response, method).await?;
if !status.is_success() {
return Err(Error::Rpc {
method: method.to_string(),
message: format!("HTTP {}: {}", status.as_u16(), truncate(&text, 256)),
code: None,
data: None,
});
}
let decoded: RpcResponse = serde_json::from_str(&text).map_err(|_| Error::Rpc {
method: method.to_string(),
message: format!("body is not a JSON-RPC response: {}", truncate(&text, 256)),
code: None,
data: None,
})?;
if let Some(error) = decoded.error {
return Err(Error::Rpc {
method: method.to_string(),
message: error.message.unwrap_or_else(|| "unknown error".to_string()),
code: error.code,
data: error.data,
});
}
Ok(decoded.result.unwrap_or(Value::Null))
}
pub async fn call(
&self,
to: &str,
data: &str,
from: Option<&str>,
) -> Result<CallOutcome, Error> {
self.call_at(to, data, from, &self.block).await
}
pub async fn call_at(
&self,
to: &str,
data: &str,
from: Option<&str>,
block: &str,
) -> Result<CallOutcome, Error> {
let mut message = json!({ "to": to, "data": data });
if let Some(from) = from {
message["from"] = Value::String(from.to_string());
}
match self.request("eth_call", json!([message, block])).await {
Ok(Value::String(result)) => Ok(CallOutcome::Ok(result)),
Ok(_) => Ok(CallOutcome::Ok("0x".to_string())),
Err(error) => match extract_revert_data(&error) {
Some(revert) => Ok(CallOutcome::Reverted(revert)),
None => Err(error),
},
}
}
pub async fn call_or_error(
&self,
to: &str,
data: &str,
context: &str,
) -> Result<String, Error> {
match self.call(to, data, None).await? {
CallOutcome::Ok(result) => Ok(result),
CallOutcome::Reverted(revert) => Err(crate::revert::revert_error(&revert, context)),
}
}
}
async fn read_capped(mut response: reqwest::Response, method: &str) -> Result<String, Error> {
let too_big = |seen: u64| Error::Rpc {
method: method.to_string(),
message: format!(
"response body is at least {seen} bytes, past the {MAX_RESPONSE_BYTES}-byte ceiling"
),
code: None,
data: None,
};
if let Some(declared) = response.content_length() {
if declared > MAX_RESPONSE_BYTES as u64 {
return Err(too_big(declared));
}
}
let mut body: Vec<u8> = Vec::new();
while let Some(chunk) = response
.chunk()
.await
.map_err(|source| Error::RpcTransport {
method: method.to_string(),
source,
})?
{
if body.len() + chunk.len() > MAX_RESPONSE_BYTES {
return Err(too_big((body.len() + chunk.len()) as u64));
}
body.extend_from_slice(&chunk);
}
Ok(String::from_utf8_lossy(&body).into_owned())
}
fn extract_revert_data(error: &Error) -> Option<String> {
let Error::Rpc { message, data, .. } = error else {
return None;
};
if let Some(data) = data {
if let Some(found) = payload_from(data) {
return Some(found);
}
if let Some(found) = payload_from(&data["data"]) {
return Some(found);
}
}
if let Some(found) = first_hex_run(message) {
return Some(found);
}
message
.to_ascii_lowercase()
.contains("revert")
.then(|| "0x".to_string())
}
fn payload_from(value: &Value) -> Option<String> {
let text = value.as_str()?;
if let Some(body) = text.strip_prefix("0x").or_else(|| text.strip_prefix("0X")) {
if body.len() % 2 == 0 && body.chars().all(|c| c.is_ascii_hexdigit()) {
return Some(format!("0x{}", body.to_ascii_lowercase()));
}
}
first_hex_run(text)
}
fn first_hex_run(text: &str) -> Option<String> {
let bytes = text.as_bytes();
let mut index = 0;
while index + 2 <= bytes.len() {
if bytes[index] == b'0' && (bytes[index + 1] == b'x' || bytes[index + 1] == b'X') {
let start = index;
let mut end = index + 2;
while end < bytes.len() && (bytes[end] as char).is_ascii_hexdigit() {
end += 1;
}
if end - start >= 10 {
return Some(text[start..end].to_ascii_lowercase());
}
index = end;
} else {
index += 1;
}
}
None
}
fn truncate(text: &str, limit: usize) -> &str {
match text.char_indices().nth(limit) {
Some((index, _)) => &text[..index],
None => text,
}
}
#[cfg(test)]
mod tests {
use super::*;
fn rpc(message: &str, data: Option<Value>) -> Error {
Error::Rpc {
method: "eth_call".to_string(),
message: message.to_string(),
code: Some(3),
data,
}
}
#[test]
fn structured_data_beats_an_address_in_the_message() {
let error = rpc(
"execution reverted: revert at 0x33a98bef6496684a8dac83734d9ceb0cefc7019c",
Some(json!("0xcfa6d878")),
);
assert_eq!(extract_revert_data(&error).as_deref(), Some("0xcfa6d878"));
}
#[test]
fn reads_the_nested_data_form() {
let error = rpc(
"execution reverted",
Some(json!({ "message": "execution reverted", "data": "0x8199F5F3" })),
);
assert_eq!(extract_revert_data(&error).as_deref(), Some("0x8199f5f3"));
}
#[test]
fn falls_back_to_the_first_run_in_the_message() {
let error = rpc("execution reverted: 0xcfa6d878 at 0xdeadbeefcafe", None);
assert_eq!(extract_revert_data(&error).as_deref(), Some("0xcfa6d878"));
}
#[test]
fn a_stripped_payload_is_still_a_revert() {
assert_eq!(
extract_revert_data(&rpc("execution reverted", Some(json!("0x")))).as_deref(),
Some("0x")
);
assert_eq!(
extract_revert_data(&rpc("execution reverted", None)).as_deref(),
Some("0x")
);
}
#[test]
fn a_plain_node_failure_is_not_a_revert() {
assert_eq!(extract_revert_data(&rpc("header not found", None)), None);
assert_eq!(
extract_revert_data(&Error::InvalidArgument {
message: "nope".to_string()
}),
None
);
}
#[test]
fn digs_a_payload_out_of_prose_in_the_data_field() {
let error = rpc(
"execution reverted",
Some(json!("Reverted 0xcfa6d878, gas 21000")),
);
assert_eq!(extract_revert_data(&error).as_deref(), Some("0xcfa6d878"));
}
}