nautilus_blockchain/contracts/
base.rs1use std::sync::Arc;
17
18use alloy::{primitives::Address, sol, sol_types::SolCall};
19use nautilus_core::hex;
20use nautilus_model::defi::validation::validate_address;
21
22use crate::rpc::{error::BlockchainRpcClientError, http::BlockchainHttpRpcClient};
23
24sol! {
25 #[sol(rpc)]
26 contract Multicall3 {
27 struct Call {
28 address target;
29 bytes callData;
30 }
31
32 struct Call3 {
33 address target;
34 bool allowFailure;
35 bytes callData;
36 }
37
38 struct Result {
39 bool success;
40 bytes returnData;
41 }
42
43 function aggregate3(Call3[] calldata calls) external payable returns (Result[] memory returnData);
44 function tryAggregate(bool requireSuccess, Call[] calldata calls) external payable returns (Result[] memory returnData);
45 }
46}
47
48pub const MULTICALL3_ADDRESS: &str = "0xcA11bde05977b3631167028862bE2a173976CA11";
50pub(crate) const DEFAULT_MULTICALL_CALLS_PER_RPC_REQUEST: u32 = 200;
51
52#[derive(Debug)]
57pub struct BaseContract {
58 client: Arc<BlockchainHttpRpcClient>,
60 multicall_address: Address,
62 multicall_calls_per_rpc_request: usize,
64 rpc_timeout_secs: Option<u64>,
66}
67
68#[derive(Debug)]
70pub struct ContractCall {
71 pub target: Address,
73 pub allow_failure: bool,
75 pub call_data: Vec<u8>,
77}
78
79impl BaseContract {
80 #[must_use]
86 pub fn new(client: Arc<BlockchainHttpRpcClient>) -> Self {
87 Self::new_with_multicall_limit(client, DEFAULT_MULTICALL_CALLS_PER_RPC_REQUEST)
88 }
89
90 #[must_use]
96 pub fn new_with_multicall_limit(
97 client: Arc<BlockchainHttpRpcClient>,
98 multicall_calls_per_rpc_request: u32,
99 ) -> Self {
100 Self::new_with_multicall_limit_and_timeout(client, multicall_calls_per_rpc_request, None)
101 }
102
103 #[must_use]
110 pub fn new_with_multicall_limit_and_timeout(
111 client: Arc<BlockchainHttpRpcClient>,
112 multicall_calls_per_rpc_request: u32,
113 rpc_timeout_secs: Option<u64>,
114 ) -> Self {
115 let multicall_address =
116 validate_address(MULTICALL3_ADDRESS).expect("Invalid multicall address");
117 let multicall_calls_per_rpc_request = (multicall_calls_per_rpc_request as usize).max(1);
118
119 Self {
120 client,
121 multicall_address,
122 multicall_calls_per_rpc_request,
123 rpc_timeout_secs,
124 }
125 }
126
127 #[must_use]
129 pub const fn client(&self) -> &Arc<BlockchainHttpRpcClient> {
130 &self.client
131 }
132
133 pub async fn execute_call(
139 &self,
140 contract_address: &Address,
141 call_data: &[u8],
142 block: Option<u64>,
143 ) -> Result<Vec<u8>, BlockchainRpcClientError> {
144 let rpc_request =
145 self.client
146 .construct_eth_call(&contract_address.to_string(), call_data, block);
147
148 self.execute_call_request(rpc_request).await
149 }
150
151 async fn execute_call_request(
152 &self,
153 rpc_request: serde_json::Value,
154 ) -> Result<Vec<u8>, BlockchainRpcClientError> {
155 let encoded_response = self
156 .client
157 .execute_rpc_call_with_timeout::<String>(rpc_request, self.rpc_timeout_secs)
158 .await
159 .map_err(|e| BlockchainRpcClientError::ClientError(format!("RPC call failed: {e}")))?;
160
161 decode_hex_response(&encoded_response)
162 }
163
164 pub async fn execute_multicall(
170 &self,
171 calls: Vec<ContractCall>,
172 block: Option<u64>,
173 ) -> Result<Vec<Multicall3::Result>, BlockchainRpcClientError> {
174 if calls.is_empty() {
175 return Ok(Vec::new());
176 }
177
178 let mut results = Vec::with_capacity(calls.len());
179 for chunk in contract_call_chunks(&calls, self.multicall_calls_per_rpc_request) {
180 results.extend(self.execute_multicall_request(chunk, block).await?);
181 }
182 Ok(results)
183 }
184
185 async fn execute_multicall_request(
186 &self,
187 calls: &[ContractCall],
188 block: Option<u64>,
189 ) -> Result<Vec<Multicall3::Result>, BlockchainRpcClientError> {
190 let multicall_calls: Vec<Multicall3::Call> = calls
192 .iter()
193 .map(|call| Multicall3::Call {
194 target: call.target,
195 callData: call.call_data.clone().into(),
196 })
197 .collect();
198
199 let multicall_data = Multicall3::tryAggregateCall {
200 requireSuccess: false,
201 calls: multicall_calls,
202 }
203 .abi_encode();
204 let rpc_request = self.client.construct_eth_call(
205 &self.multicall_address.to_string(),
206 multicall_data.as_slice(),
207 block,
208 );
209
210 let encoded_response = self
211 .client
212 .execute_rpc_call_with_timeout::<String>(rpc_request, self.rpc_timeout_secs)
213 .await
214 .map_err(|e| BlockchainRpcClientError::ClientError(format!("Multicall failed: {e}")))?;
215
216 let bytes = decode_hex_response(&encoded_response)?;
217 let results = Multicall3::tryAggregateCall::abi_decode_returns(&bytes).map_err(|e| {
218 BlockchainRpcClientError::AbiDecodingError(format!(
219 "Failed to decode multicall results: {e}"
220 ))
221 })?;
222
223 Ok(results)
224 }
225}
226
227pub fn decode_hex_response(encoded_response: &str) -> Result<Vec<u8>, BlockchainRpcClientError> {
233 let encoded_str = encoded_response
235 .strip_prefix("0x")
236 .unwrap_or(encoded_response);
237 hex::decode(encoded_str).map_err(|e| {
238 BlockchainRpcClientError::AbiDecodingError(format!("Error decoding hex response: {e}"))
239 })
240}
241
242fn contract_call_chunks(
243 calls: &[ContractCall],
244 multicall_calls_per_rpc_request: usize,
245) -> std::slice::Chunks<'_, ContractCall> {
246 calls.chunks(multicall_calls_per_rpc_request.max(1))
247}
248
249#[cfg(test)]
250mod tests {
251 use alloy::primitives::address;
252 use rstest::rstest;
253
254 use super::*;
255
256 #[rstest]
257 fn contract_call_chunks_preserves_order_across_chunk_boundary() {
258 let target = address!("25b76A90E389bD644a29db919b136Dc63B174Ec7");
259 let calls: Vec<ContractCall> = (0u8..5)
260 .map(|value| ContractCall {
261 target,
262 allow_failure: true,
263 call_data: vec![value],
264 })
265 .collect();
266
267 let chunks: Vec<Vec<u8>> = contract_call_chunks(&calls, 2)
268 .map(|chunk| chunk.iter().map(|call| call.call_data[0]).collect())
269 .collect();
270
271 assert_eq!(chunks, vec![vec![0, 1], vec![2, 3], vec![4]]);
272 }
273
274 #[rstest]
275 fn contract_call_chunks_treats_zero_limit_as_one() {
276 let target = address!("25b76A90E389bD644a29db919b136Dc63B174Ec7");
277 let calls: Vec<ContractCall> = (0u8..3)
278 .map(|value| ContractCall {
279 target,
280 allow_failure: true,
281 call_data: vec![value],
282 })
283 .collect();
284
285 let chunks: Vec<Vec<u8>> = contract_call_chunks(&calls, 0)
286 .map(|chunk| chunk.iter().map(|call| call.call_data[0]).collect())
287 .collect();
288
289 assert_eq!(chunks, vec![vec![0], vec![1], vec![2]]);
290 }
291}