Skip to main content

df_hyperliquid_core/client/evm/
rpc.rs

1//! Typed Ethereum JSON-RPC: each method is a [`Query`] that writes its request
2//! body and reads its answer, and [`Rpc`] sends one over an [`Http`] to HyperEVM
3//! ([`Network::evm_rpc_url`]) or any Arbitrum node.
4//!
5//! HyperEVM's node takes `latest` only for state reads, and an `eth_getLogs`
6//! of at most 50 blocks and 4 topics.
7
8use std::fmt;
9
10use crate::client::evm::abi::{self, Token};
11use crate::client::evm::records::{Block, FeeHistory, Log, Receipt, Transaction};
12use crate::client::evm::tx::Call;
13use crate::client::evm::u256::{B256, U256};
14use crate::client::{Http, Network, Request, TransportError};
15use crate::utils::{Address, Json, hex};
16use serde::{Deserialize, Deserializer, de::DeserializeOwned};
17use serde_json::value::RawValue;
18
19/// Which block a read is at.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
21pub enum BlockTag {
22    #[default]
23    Latest,
24    /// Counting what waits in the pool: the next nonce to use.
25    Pending,
26    Earliest,
27    Safe,
28    Finalized,
29    Number(u64),
30}
31
32impl fmt::Display for BlockTag {
33    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
34        match self {
35            BlockTag::Latest => f.write_str("latest"),
36            BlockTag::Pending => f.write_str("pending"),
37            BlockTag::Earliest => f.write_str("earliest"),
38            BlockTag::Safe => f.write_str("safe"),
39            BlockTag::Finalized => f.write_str("finalized"),
40            BlockTag::Number(number) => write!(f, "{number:#x}"),
41        }
42    }
43}
44
45/// The call object `eth_call` and `eth_estimateGas` take. No `to` estimates
46/// or runs a contract creation from `data`.
47#[derive(Debug, Clone, PartialEq, Eq, Default)]
48pub struct CallRequest {
49    pub from: Option<Address>,
50    pub to: Option<Address>,
51    pub gas: Option<u64>,
52    pub value: Option<U256>,
53    pub data: Vec<u8>,
54}
55
56impl CallRequest {
57    /// `data` sent to a contract.
58    #[must_use]
59    pub fn to(to: Address, data: Vec<u8>) -> CallRequest {
60        CallRequest { to: Some(to), data, ..CallRequest::default() }
61    }
62
63    /// What `call` does, as sent by `from`: what a transaction's gas is estimated with.
64    #[must_use]
65    pub fn of(from: Option<Address>, call: &Call) -> CallRequest {
66        CallRequest {
67            from,
68            to: call.to,
69            gas: None,
70            value: (!call.value.is_zero()).then_some(call.value),
71            data: call.data.clone(),
72        }
73    }
74
75    fn json(&self) -> Json {
76        let mut fields = Vec::new();
77        if let Some(from) = self.from {
78            fields.push(("from".to_owned(), Json::String(from.to_hex())));
79        }
80        if let Some(to) = self.to {
81            fields.push(("to".to_owned(), Json::String(to.to_hex())));
82        }
83        if let Some(gas) = self.gas {
84            fields.push(("gas".to_owned(), quantity(U256::from_u64(gas))));
85        }
86        if let Some(value) = self.value {
87            fields.push(("value".to_owned(), quantity(value)));
88        }
89        if !self.data.is_empty() {
90            fields.push(("data".to_owned(), Json::String(hex::encode_prefixed(&self.data))));
91        }
92        Json::Object(fields)
93    }
94}
95
96/// What `eth_getLogs` matches. One address is written bare and several as a
97/// list; in `topics`, `None` matches anything at its place and several
98/// topics at one place match any of them.
99#[derive(Debug, Clone, PartialEq, Eq, Default)]
100pub struct LogFilter {
101    pub from_block: Option<BlockTag>,
102    pub to_block: Option<BlockTag>,
103    /// In place of the two blocks: the one block with this hash.
104    pub block_hash: Option<B256>,
105    pub address: Vec<Address>,
106    pub topics: Vec<Option<Vec<B256>>>,
107}
108
109impl LogFilter {
110    fn json(&self) -> Json {
111        let mut fields = Vec::new();
112        if let Some(from) = self.from_block {
113            fields.push(("fromBlock".to_owned(), Json::String(from.to_string())));
114        }
115        if let Some(to) = self.to_block {
116            fields.push(("toBlock".to_owned(), Json::String(to.to_string())));
117        }
118        if let Some(hash) = self.block_hash {
119            fields.push(("blockHash".to_owned(), Json::String(hash.to_hex())));
120        }
121        match self.address.as_slice() {
122            [] => {}
123            [one] => fields.push(("address".to_owned(), Json::String(one.to_hex()))),
124            many => fields.push((
125                "address".to_owned(),
126                Json::Array(many.iter().map(|address| Json::String(address.to_hex())).collect()),
127            )),
128        }
129        if !self.topics.is_empty() {
130            let topics = self
131                .topics
132                .iter()
133                .map(|place| match place.as_deref() {
134                    None => Json::Null,
135                    Some([one]) => Json::String(one.to_hex()),
136                    Some(many) => Json::Array(many.iter().map(|topic| Json::String(topic.to_hex())).collect()),
137                })
138                .collect();
139            fields.push(("topics".to_owned(), Json::Array(topics)));
140        }
141        Json::Object(fields)
142    }
143}
144
145/// A JSON-RPC error object, as the node wrote it.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct NodeError {
148    pub code: i64,
149    pub message: String,
150    /// `data`: a string's text, or the JSON as written. A revert's data is hex.
151    pub data: Option<String>,
152}
153
154/// What a [`NodeError`]'s code says, per JSON-RPC and the Ethereum nodes.
155#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub enum ErrorKind {
157    /// -32700: the body was not JSON.
158    Parse,
159    /// -32600.
160    InvalidRequest,
161    /// -32601: the node does not serve this method.
162    MethodNotFound,
163    /// -32602.
164    InvalidParams,
165    /// -32603.
166    Internal,
167    /// 3, or a server error that says `execution reverted`.
168    Reverted,
169    /// -32000 to -32099: the node's own, a timeout or a precompile's refusal among them.
170    Server,
171    Other,
172}
173
174impl NodeError {
175    #[must_use]
176    pub fn kind(&self) -> ErrorKind {
177        match self.code {
178            -32700 => ErrorKind::Parse,
179            -32600 => ErrorKind::InvalidRequest,
180            -32601 => ErrorKind::MethodNotFound,
181            -32602 => ErrorKind::InvalidParams,
182            -32603 => ErrorKind::Internal,
183            3 => ErrorKind::Reverted,
184            -32099..=-32000 if self.message.starts_with("execution reverted") => ErrorKind::Reverted,
185            -32099..=-32000 => ErrorKind::Server,
186            _ => ErrorKind::Other,
187        }
188    }
189
190    /// Why a call reverted, when the node passed the revert data on.
191    #[must_use]
192    pub fn revert(&self) -> Option<Revert> {
193        let data = self.data.as_deref()?.strip_prefix("0x")?;
194        hex::decode(data).ok().map(|bytes| read_revert(&bytes))
195    }
196}
197
198impl fmt::Display for NodeError {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        write!(f, "{} (code {})", self.message, self.code)?;
201        match &self.data {
202            Some(data) => write!(f, ": {data}"),
203            None => Ok(()),
204        }
205    }
206}
207
208/// What a revert's data says.
209#[derive(Debug, Clone, PartialEq, Eq)]
210pub enum Revert {
211    /// `Error(string)`, from `require` and `revert("...")`.
212    Message(String),
213    /// `Panic(uint256)`: 0x11 is an overflow, 0x01 a failed assert.
214    Panic(U256),
215    /// A custom error, or nothing at all: the bytes as they came.
216    Custom(Vec<u8>),
217}
218
219/// `Error(string)`.
220const ERROR: [u8; 4] = [0x08, 0xc3, 0x79, 0xa0];
221/// `Panic(uint256)`.
222const PANIC: [u8; 4] = [0x4e, 0x48, 0x7b, 0x71];
223
224/// Reads revert data: a message, a panic code, or the bytes of anything else.
225#[must_use]
226pub fn read_revert(data: &[u8]) -> Revert {
227    let custom = || Revert::Custom(data.to_vec());
228    let Some((selector, rest)) = data.split_first_chunk::<4>() else {
229        return custom();
230    };
231    let shape = match *selector {
232        ERROR => "string",
233        PANIC => "uint256",
234        _ => return custom(),
235    };
236    let decoded = abi::params(shape).and_then(|params| abi::decode(&params, rest));
237    match decoded.as_deref() {
238        Ok([Token::String(text)]) => Revert::Message(text.clone()),
239        Ok([Token::Uint(code)]) => Revert::Panic(*code),
240        _ => custom(),
241    }
242}
243
244#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
245pub enum RpcError {
246    /// No answer, a rate limit, or an HTTP status with no JSON-RPC error in it.
247    #[error(transparent)]
248    Transport(#[from] TransportError),
249    #[error("{method}: the node refused: {error}")]
250    Node { method: &'static str, error: NodeError },
251    #[error("{method}: the node's answer could not be read: {detail}")]
252    Unreadable { method: &'static str, detail: String },
253}
254
255/// One JSON-RPC method with its params, and how its `result` is read.
256#[derive(Debug, Clone)]
257pub struct Query<T> {
258    method: &'static str,
259    params: Vec<Json>,
260    read: fn(&str) -> Result<T, String>,
261}
262
263impl<T> Query<T> {
264    #[must_use]
265    pub const fn method(&self) -> &'static str {
266        self.method
267    }
268
269    /// `{"jsonrpc":"2.0","id":1,"method":...,"params":[...]}`.
270    #[must_use]
271    pub fn body(&self) -> String {
272        Json::Object(vec![
273            ("jsonrpc".to_owned(), Json::String("2.0".to_owned())),
274            ("id".to_owned(), Json::Number("1".to_owned())),
275            ("method".to_owned(), Json::String(self.method.to_owned())),
276            ("params".to_owned(), Json::Array(self.params.clone())),
277        ])
278        .compact()
279    }
280
281    /// The answer's text, read: its `result` as `T`, or its `error`.
282    pub fn answer(&self, text: &str) -> Result<T, RpcError> {
283        let unreadable = |detail: String| RpcError::Unreadable { method: self.method, detail };
284        let envelope: Envelope<'_> = serde_json::from_str(text)
285            .map_err(|error| unreadable(format!("{error} in {}", text.chars().take(300).collect::<String>())))?;
286        if let Some(error) = envelope.error {
287            let data = error
288                .data
289                .map(|data| serde_json::from_str::<String>(data.get()).unwrap_or_else(|_| data.get().to_owned()));
290            return Err(RpcError::Node {
291                method: self.method,
292                error: NodeError { code: error.code, message: error.message, data },
293            });
294        }
295        let result = envelope.result.ok_or_else(|| unreadable("neither a result nor an error".to_owned()))?;
296        (self.read)(result.get()).map_err(|detail| unreadable(format!("{detail} in {}", result.get())))
297    }
298}
299
300#[derive(Deserialize)]
301struct Envelope<'a> {
302    #[serde(default, borrow, deserialize_with = "raw")]
303    result: Option<&'a RawValue>,
304    #[serde(default, borrow)]
305    error: Option<ErrorBody<'a>>,
306}
307
308#[derive(Deserialize)]
309struct ErrorBody<'a> {
310    code: i64,
311    #[serde(default)]
312    message: String,
313    #[serde(default, borrow, deserialize_with = "raw")]
314    data: Option<&'a RawValue>,
315}
316
317/// A value as the text it was written in, `null` included.
318fn raw<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Option<&'de RawValue>, D::Error> {
319    <&RawValue>::deserialize(deserializer).map(Some)
320}
321
322fn json<T: DeserializeOwned>(text: &str) -> Result<T, String> {
323    serde_json::from_str(text).map_err(|error| error.to_string())
324}
325
326fn small(text: &str) -> Result<u64, String> {
327    let value: U256 = json(text)?;
328    value.to_u64().ok_or_else(|| format!("{value} does not fit 64 bits"))
329}
330
331fn data(text: &str) -> Result<Vec<u8>, String> {
332    let said: String = json(text)?;
333    let digits = said.strip_prefix("0x").ok_or_else(|| format!("`{said}` is not 0x and hex"))?;
334    hex::decode(digits).map_err(|error| error.to_string())
335}
336
337fn quantity(value: U256) -> Json {
338    Json::String(value.to_quantity())
339}
340
341fn tag(block: BlockTag) -> Json {
342    Json::String(block.to_string())
343}
344
345fn address(address: Address) -> Json {
346    Json::String(address.to_hex())
347}
348
349/// Every method, as a [`Query`].
350pub mod eth {
351    use super::*;
352
353    fn query<T>(method: &'static str, params: Vec<Json>, read: fn(&str) -> Result<T, String>) -> Query<T> {
354        Query { method, params, read }
355    }
356
357    /// `eth_chainId`.
358    #[must_use]
359    pub fn chain_id() -> Query<u64> {
360        query("eth_chainId", Vec::new(), small)
361    }
362
363    /// `eth_blockNumber`.
364    #[must_use]
365    pub fn block_number() -> Query<u64> {
366        query("eth_blockNumber", Vec::new(), small)
367    }
368
369    /// `eth_getBalance`: wei of the chain's coin.
370    #[must_use]
371    pub fn balance(owner: Address, block: BlockTag) -> Query<U256> {
372        query("eth_getBalance", vec![address(owner), tag(block)], json)
373    }
374
375    /// `eth_getTransactionCount`: the next nonce, counting the pool at `Pending`.
376    #[must_use]
377    pub fn transaction_count(owner: Address, block: BlockTag) -> Query<u64> {
378        query("eth_getTransactionCount", vec![address(owner), tag(block)], small)
379    }
380
381    /// `eth_gasPrice`: on HyperEVM, the next small block's base fee.
382    #[must_use]
383    pub fn gas_price() -> Query<U256> {
384        query("eth_gasPrice", Vec::new(), json)
385    }
386
387    /// `eth_maxPriorityFeePerGas`: on HyperEVM, always zero for now.
388    #[must_use]
389    pub fn max_priority_fee() -> Query<U256> {
390        query("eth_maxPriorityFeePerGas", Vec::new(), json)
391    }
392
393    /// `eth_feeHistory` over `blocks` blocks up to `newest`, with the tip at
394    /// each whole percentile in `percentiles`, 0 to 100, rising.
395    #[must_use]
396    pub fn fee_history(blocks: u64, newest: BlockTag, percentiles: &[u8]) -> Query<FeeHistory> {
397        let percentiles = percentiles.iter().map(|percent| Json::Number(percent.to_string())).collect();
398        query("eth_feeHistory", vec![quantity(U256::from_u64(blocks)), tag(newest), Json::Array(percentiles)], json)
399    }
400
401    /// `eth_estimateGas`, a call or (with no `to`) a creation.
402    #[must_use]
403    pub fn estimate_gas(call: &CallRequest) -> Query<u64> {
404        query("eth_estimateGas", vec![call.json()], small)
405    }
406
407    /// `eth_call`: what the call returns, as bytes.
408    #[must_use]
409    pub fn call(call: &CallRequest, block: BlockTag) -> Query<Vec<u8>> {
410        query("eth_call", vec![call.json(), tag(block)], data)
411    }
412
413    /// `eth_sendRawTransaction`: the hash the node took it under.
414    #[must_use]
415    pub fn send_raw_transaction(raw: &[u8]) -> Query<B256> {
416        query("eth_sendRawTransaction", vec![Json::String(hex::encode_prefixed(raw))], json)
417    }
418
419    /// `eth_getTransactionReceipt`: `None` until it is mined.
420    #[must_use]
421    pub fn receipt(hash: B256) -> Query<Option<Receipt>> {
422        query("eth_getTransactionReceipt", vec![Json::String(hash.to_hex())], json)
423    }
424
425    /// `eth_getTransactionByHash`: `None` for a hash the node does not know.
426    #[must_use]
427    pub fn transaction(hash: B256) -> Query<Option<Transaction>> {
428        query("eth_getTransactionByHash", vec![Json::String(hash.to_hex())], json)
429    }
430
431    /// `eth_getBlockByNumber`, with whole transactions when `full`.
432    #[must_use]
433    pub fn block(block: BlockTag, full: bool) -> Query<Option<Block>> {
434        query("eth_getBlockByNumber", vec![tag(block), Json::Bool(full)], json)
435    }
436
437    /// `eth_getCode`: empty for an account that is not a contract.
438    #[must_use]
439    pub fn code(at: Address, block: BlockTag) -> Query<Vec<u8>> {
440        query("eth_getCode", vec![address(at), tag(block)], data)
441    }
442
443    /// `eth_getStorageAt`: one storage slot's word.
444    #[must_use]
445    pub fn storage_at(at: Address, slot: U256, block: BlockTag) -> Query<B256> {
446        query("eth_getStorageAt", vec![address(at), quantity(slot), tag(block)], json)
447    }
448
449    /// `eth_getLogs`.
450    #[must_use]
451    pub fn logs(filter: &LogFilter) -> Query<Vec<Log>> {
452        query("eth_getLogs", vec![filter.json()], json)
453    }
454
455    /// `eth_bigBlockGasPrice`, HyperEVM's: the next big block's base fee.
456    #[must_use]
457    pub fn big_block_gas_price() -> Query<U256> {
458        query("eth_bigBlockGasPrice", Vec::new(), json)
459    }
460
461    /// `eth_usingBigBlocks`, HyperEVM's: whether the address's transactions
462    /// go to the big blocks (`evmUserModify` on HyperCore sets it).
463    #[must_use]
464    pub fn using_big_blocks(owner: Address) -> Query<bool> {
465        query("eth_usingBigBlocks", vec![address(owner)], json)
466    }
467}
468
469/// One chain's JSON-RPC: a URL and the wire to it.
470#[derive(Debug, Clone)]
471pub struct Rpc<H> {
472    http: H,
473    url: String,
474}
475
476impl<H: Http> Rpc<H> {
477    /// Any node: an Arbitrum RPC, or HyperEVM through a provider.
478    pub fn new(http: H, url: impl Into<String>) -> Self {
479        Rpc { http, url: url.into() }
480    }
481
482    /// HyperEVM's own RPC on `network`.
483    pub fn hyperevm(http: H, network: Network) -> Self {
484        Rpc::new(http, network.evm_rpc_url())
485    }
486
487    #[must_use]
488    pub fn url(&self) -> &str {
489        &self.url
490    }
491
492    #[must_use]
493    pub const fn http(&self) -> &H {
494        &self.http
495    }
496
497    /// Posts the query once and reads the answer. Nothing is retried.
498    pub async fn run<T>(&self, query: Query<T>) -> Result<T, RpcError> {
499        let response = self.http.send(Request::post_json(self.url.clone(), query.body())).await?;
500        if response.status == 429 {
501            let seconds = response.header("retry-after").and_then(|seconds| seconds.trim().parse::<u64>().ok());
502            return Err(TransportError::RateLimited {
503                retry_after_ms: seconds.map(|seconds| seconds.saturating_mul(1_000)),
504            }
505            .into());
506        }
507        let text = response.text();
508        match (response.is_success(), query.answer(&text)) {
509            (_, Err(refused @ RpcError::Node { .. })) => Err(refused),
510            (true, answer) => answer,
511            (false, _) => Err(TransportError::Status { status: response.status, body: text }.into()),
512        }
513    }
514
515    pub async fn chain_id(&self) -> Result<u64, RpcError> {
516        self.run(eth::chain_id()).await
517    }
518
519    pub async fn block_number(&self) -> Result<u64, RpcError> {
520        self.run(eth::block_number()).await
521    }
522
523    pub async fn balance(&self, owner: Address, block: BlockTag) -> Result<U256, RpcError> {
524        self.run(eth::balance(owner, block)).await
525    }
526
527    pub async fn transaction_count(&self, owner: Address, block: BlockTag) -> Result<u64, RpcError> {
528        self.run(eth::transaction_count(owner, block)).await
529    }
530
531    pub async fn gas_price(&self) -> Result<U256, RpcError> {
532        self.run(eth::gas_price()).await
533    }
534
535    pub async fn max_priority_fee(&self) -> Result<U256, RpcError> {
536        self.run(eth::max_priority_fee()).await
537    }
538
539    pub async fn fee_history(&self, blocks: u64, newest: BlockTag, percentiles: &[u8]) -> Result<FeeHistory, RpcError> {
540        self.run(eth::fee_history(blocks, newest, percentiles)).await
541    }
542
543    pub async fn estimate_gas(&self, call: &CallRequest) -> Result<u64, RpcError> {
544        self.run(eth::estimate_gas(call)).await
545    }
546
547    pub async fn call(&self, call: &CallRequest, block: BlockTag) -> Result<Vec<u8>, RpcError> {
548        self.run(eth::call(call, block)).await
549    }
550
551    /// Sent once. With no answer the transaction may still have landed: look
552    /// for its receipt by [`crate::client::evm::tx::tx_hash`] before sending it again.
553    pub async fn send_raw_transaction(&self, raw: &[u8]) -> Result<B256, RpcError> {
554        self.run(eth::send_raw_transaction(raw)).await
555    }
556
557    pub async fn receipt(&self, hash: B256) -> Result<Option<Receipt>, RpcError> {
558        self.run(eth::receipt(hash)).await
559    }
560
561    pub async fn transaction(&self, hash: B256) -> Result<Option<Transaction>, RpcError> {
562        self.run(eth::transaction(hash)).await
563    }
564
565    pub async fn block(&self, block: BlockTag, full: bool) -> Result<Option<Block>, RpcError> {
566        self.run(eth::block(block, full)).await
567    }
568
569    pub async fn code(&self, at: Address, block: BlockTag) -> Result<Vec<u8>, RpcError> {
570        self.run(eth::code(at, block)).await
571    }
572
573    pub async fn storage_at(&self, at: Address, slot: U256, block: BlockTag) -> Result<B256, RpcError> {
574        self.run(eth::storage_at(at, slot, block)).await
575    }
576
577    pub async fn logs(&self, filter: &LogFilter) -> Result<Vec<Log>, RpcError> {
578        self.run(eth::logs(filter)).await
579    }
580
581    pub async fn big_block_gas_price(&self) -> Result<U256, RpcError> {
582        self.run(eth::big_block_gas_price()).await
583    }
584
585    pub async fn using_big_blocks(&self, owner: Address) -> Result<bool, RpcError> {
586        self.run(eth::using_big_blocks(owner)).await
587    }
588}
589
590#[cfg(test)]
591#[path = "tests/rpc.rs"]
592mod tests;