1use std::fmt;
2use std::str::FromStr;
3use std::sync::atomic::{AtomicU64, Ordering};
4use std::time::Duration;
5
6use serde_json::{Value, json};
7
8use crate::primitives::B256;
9use crate::trace::{TraceError, TxTrace, parse_traces};
10
11#[derive(Debug, thiserror::Error)]
12pub enum RpcError {
13 #[error("request to {url} failed: {source}")]
14 Transport { url: String, source: Box<ureq::Error> },
15 #[error("JSON-RPC error {code}: {message}{hint}")]
16 Rpc { code: i64, message: String, hint: &'static str },
17 #[error("malformed JSON-RPC response: {0}")]
18 Malformed(String),
19 #[error(transparent)]
20 Trace(#[from] TraceError),
21}
22
23#[derive(Clone, Debug, PartialEq, Eq)]
24pub enum BlockId {
25 Tag(&'static str),
26 Number(u64),
27 Hash(B256),
28}
29
30impl FromStr for BlockId {
31 type Err = String;
32
33 fn from_str(s: &str) -> Result<Self, String> {
34 const TAGS: [&str; 5] = ["latest", "earliest", "pending", "safe", "finalized"];
35 if let Some(tag) = TAGS.iter().find(|t| **t == s) {
36 return Ok(BlockId::Tag(tag));
37 }
38 if let Some(hex) = s.strip_prefix("0x") {
39 if hex.len() == 64 {
40 return s.parse().map(BlockId::Hash).map_err(|e| format!("{e}"));
41 }
42 return u64::from_str_radix(hex, 16)
43 .map(BlockId::Number)
44 .map_err(|_| format!("invalid block `{s}`"));
45 }
46 s.parse().map(BlockId::Number).map_err(|_| {
47 format!(
48 "invalid block `{s}`: use a number, a 0x-prefixed number or hash, or one of {}",
49 TAGS.join(", ")
50 )
51 })
52 }
53}
54
55impl fmt::Display for BlockId {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self {
58 BlockId::Tag(t) => f.write_str(t),
59 BlockId::Number(n) => write!(f, "{n}"),
60 BlockId::Hash(h) => write!(f, "{h}"),
61 }
62 }
63}
64
65#[derive(Clone, Copy, Debug, Default)]
66pub struct TracerOptions {
67 pub disable_code: bool,
68 pub disable_storage: bool,
69}
70
71impl TracerOptions {
72 pub fn to_json(self) -> Value {
73 json!({
74 "tracer": "prestateTracer",
75 "tracerConfig": {
76 "diffMode": true,
77 "disableCode": self.disable_code,
78 "disableStorage": self.disable_storage,
79 },
80 })
81 }
82}
83
84pub struct RpcClient {
86 url: String,
87 agent: ureq::Agent,
88 headers: Vec<(String, String)>,
89 retries: u32,
90 backoff: Duration,
91 next_id: AtomicU64,
92}
93
94#[derive(Clone, Debug)]
96pub struct RpcClientBuilder {
97 url: String,
98 timeout: Duration,
99 headers: Vec<(String, String)>,
100 retries: u32,
101 backoff: Duration,
102}
103
104impl RpcClientBuilder {
105 pub fn timeout(mut self, timeout: Duration) -> Self {
107 self.timeout = timeout;
108 self
109 }
110
111 pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
113 self.headers.push((name.into(), value.into()));
114 self
115 }
116
117 pub fn retries(mut self, retries: u32, backoff: Duration) -> Self {
121 self.retries = retries;
122 self.backoff = backoff;
123 self
124 }
125
126 pub fn build(self) -> RpcClient {
127 let agent = ureq::Agent::config_builder()
128 .timeout_global(Some(self.timeout))
129 .http_status_as_error(false)
130 .build();
131 RpcClient {
132 url: self.url,
133 agent: agent.into(),
134 headers: self.headers,
135 retries: self.retries,
136 backoff: self.backoff,
137 next_id: AtomicU64::new(1),
138 }
139 }
140}
141
142const RETRY_STATUS: [u16; 4] = [429, 502, 503, 504];
143
144impl RpcClient {
145 pub fn new(url: &str, timeout: Duration) -> Self {
147 Self::builder(url).timeout(timeout).build()
148 }
149
150 pub fn builder(url: &str) -> RpcClientBuilder {
151 RpcClientBuilder {
152 url: url.to_owned(),
153 timeout: Duration::from_secs(60),
154 headers: Vec::new(),
155 retries: 0,
156 backoff: Duration::from_millis(250),
157 }
158 }
159
160 pub fn call(&self, method: &str, params: Value) -> Result<Value, RpcError> {
161 let id = self.next_id.fetch_add(1, Ordering::Relaxed);
162 let body = json!({"jsonrpc": "2.0", "id": id, "method": method, "params": params});
163 let mut attempt = 0;
164 let (status, text) = loop {
165 match self.send(&body) {
166 Ok((status, _)) if RETRY_STATUS.contains(&status) && attempt < self.retries => {}
167 Err(_) if attempt < self.retries => {}
168 other => break other?,
169 }
170 std::thread::sleep(self.backoff.saturating_mul(1 << attempt.min(16)));
171 attempt += 1;
172 };
173 let value: Value = serde_json::from_str(&text).map_err(|_| {
174 let snippet: String = text.chars().take(200).collect();
175 RpcError::Malformed(format!("HTTP {status}: {snippet}"))
176 })?;
177 if let Some(err) = value.get("error") {
178 let code = err.get("code").and_then(Value::as_i64).unwrap_or(0);
179 let message = err.get("message").and_then(Value::as_str).unwrap_or("").to_owned();
180 let hint = match code {
181 -32601 => {
182 " (the node does not expose the debug namespace; enable it, e.g. geth --http.api eth,debug)"
183 }
184 -32602 => {
185 " (the node rejected the tracer options; it may not support prestateTracer diffMode)"
186 }
187 _ => "",
188 };
189 return Err(RpcError::Rpc { code, message, hint });
190 }
191 value
192 .get("result")
193 .cloned()
194 .ok_or_else(|| RpcError::Malformed(format!("HTTP {status}: no result field")))
195 }
196
197 fn send(&self, body: &Value) -> Result<(u16, String), RpcError> {
198 let transport = |e: ureq::Error| RpcError::Transport { url: self.url.clone(), source: Box::new(e) };
199 let mut req = self.agent.post(&self.url);
200 for (name, value) in &self.headers {
201 req = req.header(name, value);
202 }
203 let mut resp = req.send_json(body).map_err(transport)?;
204 let status = resp.status().as_u16();
205 let text = resp.body_mut().read_to_string().map_err(transport)?;
206 Ok((status, text))
207 }
208
209 pub fn trace_transaction(&self, hash: &B256, opts: TracerOptions) -> Result<TxTrace, RpcError> {
210 let result = self.call("debug_traceTransaction", json!([hash.to_string(), opts.to_json()]))?;
211 let mut traces = parse_traces(result)?;
212 let mut trace = traces.pop().ok_or(TraceError::UnknownFormat)?;
213 trace.tx_hash = Some(*hash);
214 Ok(trace)
215 }
216
217 pub fn trace_range(&self, from: u64, to: u64, opts: TracerOptions) -> Result<Vec<TxTrace>, RpcError> {
220 let mut traces = Vec::new();
221 for n in from..=to {
222 traces.extend(self.trace_block(&BlockId::Number(n), opts)?);
223 }
224 Ok(traces)
225 }
226
227 pub fn trace_block(&self, block: &BlockId, opts: TracerOptions) -> Result<Vec<TxTrace>, RpcError> {
228 let (method, id) = match block {
229 BlockId::Hash(h) => ("debug_traceBlockByHash", json!(h.to_string())),
230 BlockId::Number(n) => ("debug_traceBlockByNumber", json!(format!("0x{n:x}"))),
231 BlockId::Tag(t) => ("debug_traceBlockByNumber", json!(t)),
232 };
233 let result = self.call(method, json!([id, opts.to_json()]))?;
234 Ok(parse_traces(result)?)
235 }
236}
237
238#[cfg(test)]
239mod tests {
240 use super::*;
241
242 #[test]
243 fn block_ids() {
244 assert_eq!("latest".parse(), Ok(BlockId::Tag("latest")));
245 assert_eq!("12345".parse(), Ok(BlockId::Number(12345)));
246 assert_eq!("0x10".parse(), Ok(BlockId::Number(16)));
247 let hash = format!("0x{}", "ab".repeat(32));
248 assert!(matches!(hash.parse::<BlockId>(), Ok(BlockId::Hash(_))));
249 assert!("newest".parse::<BlockId>().is_err());
250 assert!("0xzz".parse::<BlockId>().is_err());
251 }
252
253 #[test]
254 fn tracer_config() {
255 let v = TracerOptions { disable_code: true, disable_storage: false }.to_json();
256 assert_eq!(v["tracer"], "prestateTracer");
257 assert_eq!(v["tracerConfig"]["diffMode"], true);
258 assert_eq!(v["tracerConfig"]["disableCode"], true);
259 }
260
261 fn serve(responses: Vec<&'static str>) -> (String, std::thread::JoinHandle<Vec<String>>) {
264 use std::io::{BufRead, BufReader, Read, Write};
265 let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
266 let url = format!("http://{}", listener.local_addr().unwrap());
267 let handle = std::thread::spawn(move || {
268 let mut heads = Vec::new();
269 for response in responses {
270 let (stream, _) = listener.accept().unwrap();
271 let mut reader = BufReader::new(stream);
272 let (mut head, mut len) = (String::new(), 0);
273 loop {
274 let mut line = String::new();
275 reader.read_line(&mut line).unwrap();
276 if let Some(v) = line.to_ascii_lowercase().strip_prefix("content-length:") {
277 len = v.trim().parse().unwrap();
278 }
279 if line == "\r\n" {
280 break;
281 }
282 head.push_str(&line);
283 }
284 reader.read_exact(&mut vec![0; len]).unwrap();
285 reader.get_mut().write_all(response.as_bytes()).unwrap();
286 heads.push(head);
287 }
288 heads
289 });
290 (url, handle)
291 }
292
293 fn http(status: &str, body: &str) -> &'static str {
294 format!("HTTP/1.1 {status}\r\ncontent-type: application/json\r\ncontent-length: {}\r\nconnection: close\r\n\r\n{body}", body.len())
295 .leak()
296 }
297
298 #[test]
299 fn retries_transient_http_errors() {
300 let ok = http("200 OK", r#"{"jsonrpc":"2.0","id":1,"result":"0x1"}"#);
301 let (url, server) =
302 serve(vec![http("503 Service Unavailable", ""), http("429 Too Many Requests", ""), ok]);
303 let client = RpcClient::builder(&url)
304 .timeout(Duration::from_secs(5))
305 .retries(2, Duration::from_millis(1))
306 .header("x-api-key", "secret")
307 .build();
308 assert_eq!(client.call("eth_chainId", json!([])).unwrap(), json!("0x1"));
309 let heads = server.join().unwrap();
310 assert_eq!(heads.len(), 3);
311 assert!(heads.iter().all(|h| h.to_ascii_lowercase().contains("x-api-key: secret")));
312 }
313
314 #[test]
315 fn gives_up_after_the_retry_budget() {
316 let (url, server) =
317 serve(vec![http("503 Service Unavailable", "busy"), http("503 Service Unavailable", "busy")]);
318 let client = RpcClient::builder(&url).retries(1, Duration::from_millis(1)).build();
319 assert!(
320 matches!(client.call("eth_chainId", json!([])), Err(RpcError::Malformed(m)) if m.contains("503"))
321 );
322 server.join().unwrap();
323 }
324
325 #[test]
326 fn json_rpc_errors_are_not_retried() {
327 let err = http("200 OK", r#"{"jsonrpc":"2.0","id":1,"error":{"code":-32601,"message":"no debug"}}"#);
328 let (url, server) = serve(vec![err]);
329 let client = RpcClient::builder(&url).retries(3, Duration::from_millis(1)).build();
330 assert!(matches!(
331 client.call("debug_traceTransaction", json!([])),
332 Err(RpcError::Rpc { code: -32601, .. })
333 ));
334 assert_eq!(server.join().unwrap().len(), 1);
335 }
336
337 #[test]
338 fn unreachable_node_is_a_transport_error() {
339 let client = RpcClient::new("http://127.0.0.1:1", Duration::from_secs(2));
340 assert!(matches!(client.call("eth_chainId", json!([])), Err(RpcError::Transport { .. })));
341 }
342}