Skip to main content

nautilus_blockchain/contracts/
base.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 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::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
48/// Standard Multicall3 address (same on all EVM chains).
49pub const MULTICALL3_ADDRESS: &str = "0xcA11bde05977b3631167028862bE2a173976CA11";
50
51/// Base contract functionality for interacting with blockchain contracts.
52///
53/// This struct provides common RPC execution patterns that can be reused
54/// by specific contract implementations like ERC20, ERC721, etc.
55#[derive(Debug)]
56pub struct BaseContract {
57    /// The HTTP RPC client used to communicate with the blockchain node.
58    client: Arc<BlockchainHttpRpcClient>,
59    /// The Multicall3 contract address.
60    multicall_address: Address,
61}
62
63/// Represents a single contract call for batching in multicall.
64#[derive(Debug)]
65pub struct ContractCall {
66    /// The target contract address
67    pub target: Address,
68    /// Whether this call can fail without reverting the entire multicall.
69    pub allow_failure: bool,
70    /// The encoded call data.
71    pub call_data: Vec<u8>,
72}
73
74impl BaseContract {
75    /// Creates a new base contract interface with the specified RPC client.
76    ///
77    /// # Panics
78    ///
79    /// Panics if the multicall address is invalid (which should never happen with the hardcoded address).
80    #[must_use]
81    pub fn new(client: Arc<BlockchainHttpRpcClient>) -> Self {
82        let multicall_address =
83            validate_address(MULTICALL3_ADDRESS).expect("Invalid multicall address");
84
85        Self {
86            client,
87            multicall_address,
88        }
89    }
90
91    /// Gets a reference to the RPC client.
92    #[must_use]
93    pub const fn client(&self) -> &Arc<BlockchainHttpRpcClient> {
94        &self.client
95    }
96
97    /// Executes a single contract call and returns the raw response bytes.
98    ///
99    /// # Errors
100    ///
101    /// Returns an error if the RPC call fails or response decoding fails.
102    pub async fn execute_call(
103        &self,
104        contract_address: &Address,
105        call_data: &[u8],
106        block: Option<u64>,
107    ) -> Result<Vec<u8>, BlockchainRpcClientError> {
108        let rpc_request =
109            self.client
110                .construct_eth_call(&contract_address.to_string(), call_data, block);
111
112        let encoded_response = self
113            .client
114            .execute_rpc_call::<String>(rpc_request)
115            .await
116            .map_err(|e| BlockchainRpcClientError::ClientError(format!("RPC call failed: {e}")))?;
117
118        decode_hex_response(&encoded_response)
119    }
120
121    /// Executes multiple contract calls in a single multicall transaction.
122    ///
123    /// # Errors
124    ///
125    /// Returns an error if the multicall fails or decoding fails.
126    pub async fn execute_multicall(
127        &self,
128        calls: Vec<ContractCall>,
129        block: Option<u64>,
130    ) -> Result<Vec<Multicall3::Result>, BlockchainRpcClientError> {
131        // Convert to Multicall3 format.
132        let multicall_calls: Vec<Multicall3::Call> = calls
133            .into_iter()
134            .map(|call| Multicall3::Call {
135                target: call.target,
136                callData: call.call_data.into(),
137            })
138            .collect();
139
140        let multicall_data = Multicall3::tryAggregateCall {
141            requireSuccess: false,
142            calls: multicall_calls,
143        }
144        .abi_encode();
145        let rpc_request = self.client.construct_eth_call(
146            &self.multicall_address.to_string(),
147            multicall_data.as_slice(),
148            block,
149        );
150
151        let encoded_response = self
152            .client
153            .execute_rpc_call::<String>(rpc_request)
154            .await
155            .map_err(|e| BlockchainRpcClientError::ClientError(format!("Multicall failed: {e}")))?;
156
157        let bytes = decode_hex_response(&encoded_response)?;
158        let results = Multicall3::tryAggregateCall::abi_decode_returns(&bytes).map_err(|e| {
159            BlockchainRpcClientError::AbiDecodingError(format!(
160                "Failed to decode multicall results: {e}"
161            ))
162        })?;
163
164        Ok(results)
165    }
166}
167
168/// Decodes a hexadecimal string response from a blockchain RPC call.
169///
170/// # Errors
171///
172/// Returns an `BlockchainRpcClientError::AbiDecodingError` if the hex decoding fails.
173pub fn decode_hex_response(encoded_response: &str) -> Result<Vec<u8>, BlockchainRpcClientError> {
174    // Remove the "0x" prefix if present
175    let encoded_str = encoded_response
176        .strip_prefix("0x")
177        .unwrap_or(encoded_response);
178    hex::decode(encoded_str).map_err(|e| {
179        BlockchainRpcClientError::AbiDecodingError(format!("Error decoding hex response: {e}"))
180    })
181}