nautilus_blockchain/rpc/http.rs
1// -------------------------------------------------------------------------------------------------
2// Copyright (C) 2015-2025 Posei Systems Pty Ltd. All rights reserved.
3// https://poseitrader.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 match self
66 .http_client
67 .request(
68 Method::POST,
69 self.http_rpc_url.clone(),
70 None,
71 Some(body_bytes),
72 None,
73 None,
74 )
75 .await
76 {
77 Ok(response) => Ok(response.body),
78 Err(e) => Err(BlockchainRpcClientError::ClientError(e.to_string())),
79 }
80 }
81
82 /// Executes an Ethereum JSON-RPC call and deserializes the response into the specified type T.
83 ///
84 /// # Errors
85 ///
86 /// Returns an error if the HTTP RPC request fails or the response cannot be parsed.
87 pub async fn execute_eth_call<T: DeserializeOwned>(
88 &self,
89 rpc_request: serde_json::Value,
90 ) -> anyhow::Result<T> {
91 match self.send_rpc_request(rpc_request).await {
92 Ok(bytes) => match serde_json::from_slice::<RpcNodeHttpResponse<T>>(bytes.as_ref()) {
93 Ok(parsed) => Ok(parsed.result),
94 Err(e) => Err(anyhow::anyhow!("Failed to parse eth call response: {}", e)),
95 },
96 Err(e) => Err(anyhow::anyhow!(
97 "Failed to execute eth call RPC request: {}",
98 e
99 )),
100 }
101 }
102
103 /// Creates a properly formatted `eth_call` JSON-RPC request object targeting a specific contract address with encoded function data.
104 #[must_use]
105 pub fn construct_eth_call(&self, to: &str, call_data: &[u8]) -> serde_json::Value {
106 let encoded_data = hex::encode(call_data);
107 let call = serde_json::json!({
108 "to": to,
109 "data": encoded_data
110 });
111
112 serde_json::json!({
113 "jsonrpc": "2.0",
114 "id": 1,
115 "method": "eth_call",
116 "params": [call]
117 })
118 }
119}