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";
50const 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}
65
66#[derive(Debug)]
68pub struct ContractCall {
69 pub target: Address,
71 pub allow_failure: bool,
73 pub call_data: Vec<u8>,
75}
76
77impl BaseContract {
78 #[must_use]
84 pub fn new(client: Arc<BlockchainHttpRpcClient>) -> Self {
85 Self::new_with_multicall_limit(client, DEFAULT_MULTICALL_CALLS_PER_RPC_REQUEST)
86 }
87
88 #[must_use]
94 pub fn new_with_multicall_limit(
95 client: Arc<BlockchainHttpRpcClient>,
96 multicall_calls_per_rpc_request: u32,
97 ) -> Self {
98 let multicall_address =
99 validate_address(MULTICALL3_ADDRESS).expect("Invalid multicall address");
100 let multicall_calls_per_rpc_request = (multicall_calls_per_rpc_request as usize).max(1);
101
102 Self {
103 client,
104 multicall_address,
105 multicall_calls_per_rpc_request,
106 }
107 }
108
109 #[must_use]
111 pub const fn client(&self) -> &Arc<BlockchainHttpRpcClient> {
112 &self.client
113 }
114
115 pub async fn execute_call(
121 &self,
122 contract_address: &Address,
123 call_data: &[u8],
124 block: Option<u64>,
125 ) -> Result<Vec<u8>, BlockchainRpcClientError> {
126 let rpc_request =
127 self.client
128 .construct_eth_call(&contract_address.to_string(), call_data, block);
129
130 let encoded_response = self
131 .client
132 .execute_rpc_call::<String>(rpc_request)
133 .await
134 .map_err(|e| BlockchainRpcClientError::ClientError(format!("RPC call failed: {e}")))?;
135
136 decode_hex_response(&encoded_response)
137 }
138
139 pub async fn execute_multicall(
145 &self,
146 calls: Vec<ContractCall>,
147 block: Option<u64>,
148 ) -> Result<Vec<Multicall3::Result>, BlockchainRpcClientError> {
149 if calls.is_empty() {
150 return Ok(Vec::new());
151 }
152
153 let mut results = Vec::with_capacity(calls.len());
154 for chunk in contract_call_chunks(&calls, self.multicall_calls_per_rpc_request) {
155 results.extend(self.execute_multicall_request(chunk, block).await?);
156 }
157 Ok(results)
158 }
159
160 async fn execute_multicall_request(
161 &self,
162 calls: &[ContractCall],
163 block: Option<u64>,
164 ) -> Result<Vec<Multicall3::Result>, BlockchainRpcClientError> {
165 let multicall_calls: Vec<Multicall3::Call> = calls
167 .iter()
168 .map(|call| Multicall3::Call {
169 target: call.target,
170 callData: call.call_data.clone().into(),
171 })
172 .collect();
173
174 let multicall_data = Multicall3::tryAggregateCall {
175 requireSuccess: false,
176 calls: multicall_calls,
177 }
178 .abi_encode();
179 let rpc_request = self.client.construct_eth_call(
180 &self.multicall_address.to_string(),
181 multicall_data.as_slice(),
182 block,
183 );
184
185 let encoded_response = self
186 .client
187 .execute_rpc_call::<String>(rpc_request)
188 .await
189 .map_err(|e| BlockchainRpcClientError::ClientError(format!("Multicall failed: {e}")))?;
190
191 let bytes = decode_hex_response(&encoded_response)?;
192 let results = Multicall3::tryAggregateCall::abi_decode_returns(&bytes).map_err(|e| {
193 BlockchainRpcClientError::AbiDecodingError(format!(
194 "Failed to decode multicall results: {e}"
195 ))
196 })?;
197
198 Ok(results)
199 }
200}
201
202pub fn decode_hex_response(encoded_response: &str) -> Result<Vec<u8>, BlockchainRpcClientError> {
208 let encoded_str = encoded_response
210 .strip_prefix("0x")
211 .unwrap_or(encoded_response);
212 hex::decode(encoded_str).map_err(|e| {
213 BlockchainRpcClientError::AbiDecodingError(format!("Error decoding hex response: {e}"))
214 })
215}
216
217fn contract_call_chunks(
218 calls: &[ContractCall],
219 multicall_calls_per_rpc_request: usize,
220) -> std::slice::Chunks<'_, ContractCall> {
221 calls.chunks(multicall_calls_per_rpc_request.max(1))
222}
223
224#[cfg(test)]
225mod tests {
226 use alloy::primitives::address;
227 use rstest::rstest;
228
229 use super::*;
230
231 #[rstest]
232 fn contract_call_chunks_preserves_order_across_chunk_boundary() {
233 let target = address!("25b76A90E389bD644a29db919b136Dc63B174Ec7");
234 let calls: Vec<ContractCall> = (0u8..5)
235 .map(|value| ContractCall {
236 target,
237 allow_failure: true,
238 call_data: vec![value],
239 })
240 .collect();
241
242 let chunks: Vec<Vec<u8>> = contract_call_chunks(&calls, 2)
243 .map(|chunk| chunk.iter().map(|call| call.call_data[0]).collect())
244 .collect();
245
246 assert_eq!(chunks, vec![vec![0, 1], vec![2, 3], vec![4]]);
247 }
248
249 #[rstest]
250 fn contract_call_chunks_treats_zero_limit_as_one() {
251 let target = address!("25b76A90E389bD644a29db919b136Dc63B174Ec7");
252 let calls: Vec<ContractCall> = (0u8..3)
253 .map(|value| ContractCall {
254 target,
255 allow_failure: true,
256 call_data: vec![value],
257 })
258 .collect();
259
260 let chunks: Vec<Vec<u8>> = contract_call_chunks(&calls, 0)
261 .map(|chunk| chunk.iter().map(|call| call.call_data[0]).collect())
262 .collect();
263
264 assert_eq!(chunks, vec![vec![0], vec![1], vec![2]]);
265 }
266}