Skip to main content

quicknode_hyperliquid_sdk/
evm.rs

1//! EVM (HyperEVM) client for Hyperliquid.
2//!
3//! Provides Ethereum JSON-RPC compatibility for the Hyperliquid EVM.
4
5use serde_json::{json, Value};
6use std::sync::Arc;
7
8use crate::client::HyperliquidSDKInner;
9use crate::error::Result;
10
11/// EVM API client (Ethereum JSON-RPC)
12pub struct EVM {
13    inner: Arc<HyperliquidSDKInner>,
14    debug: bool,
15}
16
17impl EVM {
18    pub(crate) fn new(inner: Arc<HyperliquidSDKInner>) -> Self {
19        Self {
20            inner,
21            debug: false,
22        }
23    }
24
25    /// Enable debug/trace methods (mainnet only)
26    pub fn with_debug(mut self, debug: bool) -> Self {
27        self.debug = debug;
28        self
29    }
30
31    /// Get the EVM endpoint URL
32    fn evm_url(&self) -> String {
33        self.inner.evm_url(self.debug)
34    }
35
36    /// Make a JSON-RPC request
37    async fn rpc(&self, method: &str, params: Value) -> Result<Value> {
38        let url = self.evm_url();
39
40        let body = json!({
41            "jsonrpc": "2.0",
42            "method": method,
43            "params": params,
44            "id": 1,
45        });
46
47        let response = self
48            .inner
49            .http_client
50            .post(&url)
51            .json(&body)
52            .send()
53            .await?;
54
55        let status = response.status();
56        let text = response.text().await?;
57
58        if !status.is_success() {
59            return Err(crate::Error::NetworkError(format!(
60                "EVM request failed {}: {}",
61                status, text
62            )));
63        }
64
65        let result: Value = serde_json::from_str(&text)?;
66
67        // Check for JSON-RPC error
68        if let Some(error) = result.get("error") {
69            let message = error
70                .get("message")
71                .and_then(|m| m.as_str())
72                .unwrap_or("Unknown error");
73            return Err(crate::Error::ApiError {
74                code: crate::error::ErrorCode::Unknown,
75                message: message.to_string(),
76                guidance: "Check your EVM request parameters.".to_string(),
77                raw: Some(error.to_string()),
78            });
79        }
80
81        Ok(result.get("result").cloned().unwrap_or(Value::Null))
82    }
83
84    // ──────────────────────────────────────────────────────────────────────────
85    // Chain Info
86    // ──────────────────────────────────────────────────────────────────────────
87
88    /// Get the current block number
89    pub async fn block_number(&self) -> Result<u64> {
90        let result = self.rpc("eth_blockNumber", json!([])).await?;
91        parse_hex_u64(&result)
92    }
93
94    /// Get the chain ID
95    pub async fn chain_id(&self) -> Result<u64> {
96        let result = self.rpc("eth_chainId", json!([])).await?;
97        parse_hex_u64(&result)
98    }
99
100    /// Check if the node is syncing
101    pub async fn syncing(&self) -> Result<Value> {
102        self.rpc("eth_syncing", json!([])).await
103    }
104
105    /// Get current gas price
106    pub async fn gas_price(&self) -> Result<u64> {
107        let result = self.rpc("eth_gasPrice", json!([])).await?;
108        parse_hex_u64(&result)
109    }
110
111    /// Get network version
112    pub async fn net_version(&self) -> Result<String> {
113        let result = self.rpc("net_version", json!([])).await?;
114        Ok(result.as_str().unwrap_or("").to_string())
115    }
116
117    /// Get client version
118    pub async fn web3_client_version(&self) -> Result<String> {
119        let result = self.rpc("web3_clientVersion", json!([])).await?;
120        Ok(result.as_str().unwrap_or("").to_string())
121    }
122
123    // ──────────────────────────────────────────────────────────────────────────
124    // Account
125    // ──────────────────────────────────────────────────────────────────────────
126
127    /// Get balance of an address
128    pub async fn get_balance(&self, address: &str, block: Option<&str>) -> Result<String> {
129        let block = block.unwrap_or("latest");
130        let result = self.rpc("eth_getBalance", json!([address, block])).await?;
131        Ok(result.as_str().unwrap_or("0x0").to_string())
132    }
133
134    /// Get transaction count (nonce) of an address
135    pub async fn get_transaction_count(&self, address: &str, block: Option<&str>) -> Result<u64> {
136        let block = block.unwrap_or("latest");
137        let result = self
138            .rpc("eth_getTransactionCount", json!([address, block]))
139            .await?;
140        parse_hex_u64(&result)
141    }
142
143    /// Get code at an address
144    pub async fn get_code(&self, address: &str, block: Option<&str>) -> Result<String> {
145        let block = block.unwrap_or("latest");
146        let result = self.rpc("eth_getCode", json!([address, block])).await?;
147        Ok(result.as_str().unwrap_or("0x").to_string())
148    }
149
150    /// Get storage at a position
151    pub async fn get_storage_at(
152        &self,
153        address: &str,
154        position: &str,
155        block: Option<&str>,
156    ) -> Result<String> {
157        let block = block.unwrap_or("latest");
158        let result = self
159            .rpc("eth_getStorageAt", json!([address, position, block]))
160            .await?;
161        Ok(result.as_str().unwrap_or("0x0").to_string())
162    }
163
164    /// Get list of accounts (usually empty for remote nodes)
165    pub async fn accounts(&self) -> Result<Vec<String>> {
166        let result = self.rpc("eth_accounts", json!([])).await?;
167        Ok(result
168            .as_array()
169            .map(|arr| {
170                arr.iter()
171                    .filter_map(|v| v.as_str().map(|s| s.to_string()))
172                    .collect()
173            })
174            .unwrap_or_default())
175    }
176
177    // ──────────────────────────────────────────────────────────────────────────
178    // Transactions
179    // ──────────────────────────────────────────────────────────────────────────
180
181    /// Call a contract (read-only)
182    pub async fn call(&self, tx: &Value, block: Option<&str>) -> Result<String> {
183        let block = block.unwrap_or("latest");
184        let result = self.rpc("eth_call", json!([tx, block])).await?;
185        Ok(result.as_str().unwrap_or("0x").to_string())
186    }
187
188    /// Estimate gas for a transaction
189    pub async fn estimate_gas(&self, tx: &Value) -> Result<u64> {
190        let result = self.rpc("eth_estimateGas", json!([tx])).await?;
191        parse_hex_u64(&result)
192    }
193
194    /// Send a raw (signed) transaction
195    pub async fn send_raw_transaction(&self, signed_tx: &str) -> Result<String> {
196        let result = self.rpc("eth_sendRawTransaction", json!([signed_tx])).await?;
197        Ok(result.as_str().unwrap_or("").to_string())
198    }
199
200    /// Get transaction by hash
201    pub async fn get_transaction_by_hash(&self, tx_hash: &str) -> Result<Value> {
202        self.rpc("eth_getTransactionByHash", json!([tx_hash])).await
203    }
204
205    /// Get transaction receipt
206    pub async fn get_transaction_receipt(&self, tx_hash: &str) -> Result<Value> {
207        self.rpc("eth_getTransactionReceipt", json!([tx_hash])).await
208    }
209
210    // ──────────────────────────────────────────────────────────────────────────
211    // Blocks
212    // ──────────────────────────────────────────────────────────────────────────
213
214    /// Get block by number
215    pub async fn get_block_by_number(
216        &self,
217        block_number: &str,
218        full_transactions: bool,
219    ) -> Result<Value> {
220        self.rpc(
221            "eth_getBlockByNumber",
222            json!([block_number, full_transactions]),
223        )
224        .await
225    }
226
227    /// Get block by hash
228    pub async fn get_block_by_hash(&self, block_hash: &str, full_transactions: bool) -> Result<Value> {
229        self.rpc(
230            "eth_getBlockByHash",
231            json!([block_hash, full_transactions]),
232        )
233        .await
234    }
235
236    // ──────────────────────────────────────────────────────────────────────────
237    // Logs
238    // ──────────────────────────────────────────────────────────────────────────
239
240    /// Get logs matching a filter
241    pub async fn get_logs(&self, filter: &Value) -> Result<Value> {
242        self.rpc("eth_getLogs", json!([filter])).await
243    }
244
245    // ──────────────────────────────────────────────────────────────────────────
246    // Fees
247    // ──────────────────────────────────────────────────────────────────────────
248
249    /// Get fee history
250    pub async fn fee_history(
251        &self,
252        block_count: u64,
253        newest_block: &str,
254        reward_percentiles: Option<&[f64]>,
255    ) -> Result<Value> {
256        let percentiles = reward_percentiles.unwrap_or(&[]);
257        self.rpc(
258            "eth_feeHistory",
259            json!([format!("0x{:x}", block_count), newest_block, percentiles]),
260        )
261        .await
262    }
263
264    /// Get max priority fee per gas
265    pub async fn max_priority_fee_per_gas(&self) -> Result<u64> {
266        let result = self.rpc("eth_maxPriorityFeePerGas", json!([])).await?;
267        parse_hex_u64(&result)
268    }
269
270    /// Get block receipts
271    pub async fn get_block_receipts(&self, block_number: &str) -> Result<Value> {
272        self.rpc("eth_getBlockReceipts", json!([block_number])).await
273    }
274
275    /// Get block transaction count by hash
276    pub async fn get_block_transaction_count_by_hash(&self, block_hash: &str) -> Result<u64> {
277        let result = self
278            .rpc("eth_getBlockTransactionCountByHash", json!([block_hash]))
279            .await?;
280        parse_hex_u64(&result)
281    }
282
283    /// Get block transaction count by number
284    pub async fn get_block_transaction_count_by_number(&self, block_number: &str) -> Result<u64> {
285        let result = self
286            .rpc("eth_getBlockTransactionCountByNumber", json!([block_number]))
287            .await?;
288        parse_hex_u64(&result)
289    }
290
291    /// Get transaction by block hash and index
292    pub async fn get_transaction_by_block_hash_and_index(
293        &self,
294        block_hash: &str,
295        index: u64,
296    ) -> Result<Value> {
297        self.rpc(
298            "eth_getTransactionByBlockHashAndIndex",
299            json!([block_hash, format!("0x{:x}", index)]),
300        )
301        .await
302    }
303
304    /// Get transaction by block number and index
305    pub async fn get_transaction_by_block_number_and_index(
306        &self,
307        block_number: &str,
308        index: u64,
309    ) -> Result<Value> {
310        self.rpc(
311            "eth_getTransactionByBlockNumberAndIndex",
312            json!([block_number, format!("0x{:x}", index)]),
313        )
314        .await
315    }
316
317    // ──────────────────────────────────────────────────────────────────────────
318    // Debug/Trace (requires debug=true, mainnet only)
319    // ──────────────────────────────────────────────────────────────────────────
320
321    /// Debug trace a transaction
322    pub async fn debug_trace_transaction(
323        &self,
324        tx_hash: &str,
325        tracer_config: Option<&Value>,
326    ) -> Result<Value> {
327        if !self.debug {
328            return Err(crate::Error::ConfigError(
329                "Debug mode not enabled. Use .with_debug(true)".to_string(),
330            ));
331        }
332        let config = tracer_config.cloned().unwrap_or(json!({}));
333        self.rpc("debug_traceTransaction", json!([tx_hash, config]))
334            .await
335    }
336
337    /// Trace a transaction
338    pub async fn trace_transaction(&self, tx_hash: &str) -> Result<Value> {
339        if !self.debug {
340            return Err(crate::Error::ConfigError(
341                "Debug mode not enabled. Use .with_debug(true)".to_string(),
342            ));
343        }
344        self.rpc("trace_transaction", json!([tx_hash])).await
345    }
346
347    /// Trace a block
348    pub async fn trace_block(&self, block_number: &str) -> Result<Value> {
349        if !self.debug {
350            return Err(crate::Error::ConfigError(
351                "Debug mode not enabled. Use .with_debug(true)".to_string(),
352            ));
353        }
354        self.rpc("trace_block", json!([block_number])).await
355    }
356}
357
358/// Parse a hex string to u64
359fn parse_hex_u64(value: &Value) -> Result<u64> {
360    let s = value.as_str().unwrap_or("0x0");
361    let s = s.trim_start_matches("0x");
362    u64::from_str_radix(s, 16)
363        .map_err(|e| crate::Error::JsonError(format!("Invalid hex number: {}", e)))
364}