nautilus_blockchain/rpc/
http.rs1use std::{collections::HashMap, num::NonZeroU32, str::FromStr};
17
18use alloy::primitives::{Address, U256};
19use bytes::Bytes;
20use nautilus_model::defi::rpc::RpcNodeHttpResponse;
21use nautilus_network::{http::HttpClient, ratelimiter::quota::Quota};
22use reqwest::Method;
23use serde::de::DeserializeOwned;
24
25use crate::rpc::error::BlockchainRpcClientError;
26
27#[derive(Debug)]
32pub struct BlockchainHttpRpcClient {
33 http_rpc_url: String,
35 http_client: HttpClient,
37}
38
39impl BlockchainHttpRpcClient {
40 #[must_use]
46 pub fn new(http_rpc_url: String, rpc_request_per_second: Option<u32>) -> Self {
47 let default_quota = rpc_request_per_second.map(|rpc_request_per_second| {
48 Quota::per_second(NonZeroU32::new(rpc_request_per_second).unwrap())
49 });
50 let http_client = HttpClient::new(
51 HashMap::new(),
52 vec![],
53 Vec::new(),
54 default_quota,
55 None, None, )
58 .expect("Failed to create HTTP client");
59 Self {
60 http_rpc_url,
61 http_client,
62 }
63 }
64
65 async fn send_rpc_request(
67 &self,
68 rpc_request: serde_json::Value,
69 ) -> Result<Bytes, BlockchainRpcClientError> {
70 let body_bytes = serde_json::to_vec(&rpc_request).map_err(|e| {
71 BlockchainRpcClientError::ClientError(format!("Failed to serialize request: {e}"))
72 })?;
73
74 let mut headers = HashMap::new();
75 headers.insert("Content-Type".to_string(), "application/json".to_string());
76
77 match self
78 .http_client
79 .request(
80 Method::POST,
81 self.http_rpc_url.clone(),
82 None,
83 Some(headers),
84 Some(body_bytes),
85 None,
86 None,
87 )
88 .await
89 {
90 Ok(response) => Ok(response.body),
91 Err(e) => Err(BlockchainRpcClientError::ClientError(e.to_string())),
92 }
93 }
94
95 pub async fn execute_rpc_call<T: DeserializeOwned>(
101 &self,
102 rpc_request: serde_json::Value,
103 ) -> anyhow::Result<T> {
104 match self.send_rpc_request(rpc_request).await {
105 Ok(bytes) => match serde_json::from_slice::<RpcNodeHttpResponse<T>>(bytes.as_ref()) {
106 Ok(parsed) => {
107 if let Some(error) = parsed.error {
108 Err(anyhow::anyhow!(
109 "RPC error {}: {}",
110 error.code,
111 error.message
112 ))
113 } else if let Some(result) = parsed.result {
114 Ok(result)
115 } else {
116 Err(anyhow::anyhow!(
117 "Response missing both result and error fields"
118 ))
119 }
120 }
121 Err(e) => {
122 let raw_response = String::from_utf8_lossy(bytes.as_ref());
124 let preview = if raw_response.len() > 500 {
125 format!(
126 "{}... (truncated, {} bytes total)",
127 &raw_response[..500],
128 raw_response.len()
129 )
130 } else {
131 raw_response.to_string()
132 };
133
134 Err(anyhow::anyhow!(
135 "Failed to parse eth call response: {}\nRaw response: {}",
136 e,
137 preview
138 ))
139 }
140 },
141 Err(e) => Err(anyhow::anyhow!(
142 "Failed to execute eth call RPC request: {}",
143 e
144 )),
145 }
146 }
147
148 #[must_use]
150 pub fn construct_eth_call(
151 &self,
152 to: &str,
153 call_data: &[u8],
154 block: Option<u64>,
155 ) -> serde_json::Value {
156 let encoded_data = format!("0x{}", hex::encode(call_data));
157 let call = serde_json::json!({
158 "to": to,
159 "data": encoded_data
160 });
161
162 let block_param = if let Some(block_number) = block {
163 serde_json::json!(format!("0x{:x}", block_number))
164 } else {
165 serde_json::json!("latest")
166 };
167
168 serde_json::json!({
169 "jsonrpc": "2.0",
170 "id": 1,
171 "method": "eth_call",
172 "params": [call, block_param]
173 })
174 }
175
176 pub async fn get_balance(&self, address: &Address, block: Option<u64>) -> anyhow::Result<U256> {
182 let block_param = if let Some(block_number) = block {
183 serde_json::json!(format!("0x{:x}", block_number))
184 } else {
185 serde_json::json!("latest")
186 };
187
188 let request = serde_json::json!({
189 "jsonrpc": "2.0",
190 "id": 1,
191 "method": "eth_getBalance",
192 "params": [address, block_param]
193 });
194 let hex_string: String = self.execute_rpc_call(request).await?;
195
196 U256::from_str(&hex_string).map_err(|e| {
197 anyhow::anyhow!("Failed to parse balance hex string '{}': {}", hex_string, e)
198 })
199 }
200}