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