nautilus_blockchain/contracts/
base.rs

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