nautilus_blockchain/rpc/
http.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2025 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16use std::{collections::HashMap, num::NonZeroU32};
17
18use bytes::Bytes;
19use nautilus_model::defi::rpc::RpcNodeHttpResponse;
20use nautilus_network::{http::HttpClient, ratelimiter::quota::Quota};
21use reqwest::Method;
22use serde::de::DeserializeOwned;
23
24use crate::rpc::error::BlockchainRpcClientError;
25
26/// Client for making HTTP-based RPC requests to blockchain nodes.
27///
28/// This client is designed to interact with Ethereum-compatible blockchain networks, providing
29/// methods to execute RPC calls and handle responses in a type-safe manner.
30#[derive(Debug)]
31pub struct BlockchainHttpRpcClient {
32    /// The HTTP URL for the blockchain node's RPC endpoint.
33    http_rpc_url: String,
34    /// The HTTP client for making RPC http-based requests.
35    http_client: HttpClient,
36}
37
38impl BlockchainHttpRpcClient {
39    /// Creates a new HTTP RPC client with the given endpoint URL and optional rate limit.
40    ///
41    /// # Panics
42    ///
43    /// Panics if `rpc_request_per_second` is `Some(0)`, since a zero rate limit is invalid.
44    #[must_use]
45    pub fn new(http_rpc_url: String, rpc_request_per_second: Option<u32>) -> Self {
46        let default_quota = rpc_request_per_second.map(|rpc_request_per_second| {
47            Quota::per_second(NonZeroU32::new(rpc_request_per_second).unwrap())
48        });
49        let http_client = HttpClient::new(HashMap::new(), vec![], Vec::new(), default_quota, None);
50        Self {
51            http_rpc_url,
52            http_client,
53        }
54    }
55
56    /// Generic method that sends a JSON-RPC request and returns the raw response in bytes.
57    async fn send_rpc_request(
58        &self,
59        rpc_request: serde_json::Value,
60    ) -> Result<Bytes, BlockchainRpcClientError> {
61        let body_bytes = serde_json::to_vec(&rpc_request).map_err(|e| {
62            BlockchainRpcClientError::ClientError(format!("Failed to serialize request: {e}"))
63        })?;
64
65        let mut headers = HashMap::new();
66        headers.insert("Content-Type".to_string(), "application/json".to_string());
67
68        match self
69            .http_client
70            .request(
71                Method::POST,
72                self.http_rpc_url.clone(),
73                Some(headers),
74                Some(body_bytes),
75                None,
76                None,
77            )
78            .await
79        {
80            Ok(response) => Ok(response.body),
81            Err(e) => Err(BlockchainRpcClientError::ClientError(e.to_string())),
82        }
83    }
84
85    /// Executes an Ethereum JSON-RPC call and deserializes the response into the specified type T.
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if the HTTP RPC request fails or the response cannot be parsed.
90    pub async fn execute_eth_call<T: DeserializeOwned>(
91        &self,
92        rpc_request: serde_json::Value,
93    ) -> anyhow::Result<T> {
94        match self.send_rpc_request(rpc_request).await {
95            Ok(bytes) => match serde_json::from_slice::<RpcNodeHttpResponse<T>>(bytes.as_ref()) {
96                Ok(parsed) => {
97                    if let Some(error) = parsed.error {
98                        Err(anyhow::anyhow!(
99                            "RPC error {}: {}",
100                            error.code,
101                            error.message
102                        ))
103                    } else if let Some(result) = parsed.result {
104                        Ok(result)
105                    } else {
106                        Err(anyhow::anyhow!(
107                            "Response missing both result and error fields"
108                        ))
109                    }
110                }
111                Err(e) => Err(anyhow::anyhow!("Failed to parse eth call response: {}", e)),
112            },
113            Err(e) => Err(anyhow::anyhow!(
114                "Failed to execute eth call RPC request: {}",
115                e
116            )),
117        }
118    }
119
120    /// Creates a properly formatted `eth_call` JSON-RPC request object targeting a specific contract address with encoded function data.
121    #[must_use]
122    pub fn construct_eth_call(&self, to: &str, call_data: &[u8]) -> serde_json::Value {
123        let encoded_data = format!("0x{}", hex::encode(call_data));
124        let call = serde_json::json!({
125            "to": to,
126            "data": encoded_data
127        });
128
129        serde_json::json!({
130            "jsonrpc": "2.0",
131            "id": 1,
132            "method": "eth_call",
133            "params": [call, "latest"]
134        })
135    }
136}