Skip to main content

sal_vault/ethereum/
contract.rs

1//! Smart contract interaction functionality.
2//!
3//! This module provides functionality for interacting with smart contracts on EVM-based blockchains.
4
5use ethers::abi::{Abi, Token};
6use ethers::prelude::*;
7use serde::{Deserialize, Serialize};
8use std::str::FromStr;
9use std::sync::Arc;
10
11use super::networks::NetworkConfig;
12use super::wallet::EthereumWallet;
13use crate::error::CryptoError;
14
15/// A smart contract instance.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Contract {
18    /// The contract address
19    pub address: Address,
20    /// The contract ABI
21    pub abi: Abi,
22    /// The network the contract is deployed on
23    pub network: NetworkConfig,
24}
25
26impl Contract {
27    /// Creates a new contract instance.
28    pub fn new(address: Address, abi: Abi, network: NetworkConfig) -> Self {
29        Contract {
30            address,
31            abi,
32            network,
33        }
34    }
35
36    /// Creates a new contract instance from an address string and ABI.
37    pub fn from_address_string(
38        address_str: &str,
39        abi: Abi,
40        network: NetworkConfig,
41    ) -> Result<Self, CryptoError> {
42        let address = Address::from_str(address_str)
43            .map_err(|e| CryptoError::InvalidAddress(format!("Invalid address format: {}", e)))?;
44
45        Ok(Contract::new(address, abi, network))
46    }
47
48    /// Creates an ethers Contract instance for interaction.
49    pub fn create_ethers_contract(
50        &self,
51        provider: Provider<Http>,
52        _wallet: Option<&EthereumWallet>,
53    ) -> Result<ethers::contract::Contract<ethers::providers::Provider<Http>>, CryptoError> {
54        let contract =
55            ethers::contract::Contract::new(self.address, self.abi.clone(), Arc::new(provider));
56
57        Ok(contract)
58    }
59}
60
61/// Loads a contract ABI from a JSON string.
62pub fn load_abi_from_json(json_str: &str) -> Result<Abi, CryptoError> {
63    serde_json::from_str(json_str)
64        .map_err(|e| CryptoError::SerializationError(format!("Failed to parse ABI JSON: {}", e)))
65}
66
67/// Calls a read-only function on a contract.
68pub async fn call_read_function(
69    contract: &Contract,
70    provider: &Provider<Http>,
71    function_name: &str,
72    args: Vec<Token>,
73) -> Result<Vec<Token>, CryptoError> {
74    // Create the ethers contract (not used directly but kept for future extensions)
75    let _ethers_contract = contract.create_ethers_contract(provider.clone(), None)?;
76
77    // Get the function from the ABI
78    let function = contract
79        .abi
80        .function(function_name)
81        .map_err(|e| CryptoError::ContractError(format!("Function not found in ABI: {}", e)))?;
82
83    // Encode the function call
84    let call_data = function.encode_input(&args).map_err(|e| {
85        CryptoError::ContractError(format!("Failed to encode function call: {}", e))
86    })?;
87
88    // Make the call
89    let tx = TransactionRequest::new()
90        .to(contract.address)
91        .data(call_data);
92
93    let result = provider
94        .call(&tx.into(), None)
95        .await
96        .map_err(|e| CryptoError::ContractError(format!("Contract call failed: {}", e)))?;
97
98    // Decode the result
99    let decoded = function.decode_output(&result).map_err(|e| {
100        CryptoError::ContractError(format!("Failed to decode function output: {}", e))
101    })?;
102
103    Ok(decoded)
104}
105
106/// Executes a state-changing function on a contract.
107pub async fn call_write_function(
108    contract: &Contract,
109    wallet: &EthereumWallet,
110    provider: &Provider<Http>,
111    function_name: &str,
112    args: Vec<Token>,
113) -> Result<H256, CryptoError> {
114    // Create a client with the wallet
115    let client = SignerMiddleware::new(provider.clone(), wallet.wallet.clone());
116
117    // Get the function from the ABI
118    let function = contract
119        .abi
120        .function(function_name)
121        .map_err(|e| CryptoError::ContractError(format!("Function not found in ABI: {}", e)))?;
122
123    // Encode the function call
124    let call_data = function.encode_input(&args).map_err(|e| {
125        CryptoError::ContractError(format!("Failed to encode function call: {}", e))
126    })?;
127
128    // Create the transaction request with gas limit
129    let tx = TransactionRequest::new()
130        .to(contract.address)
131        .data(call_data)
132        .gas(U256::from(300000)); // Set a reasonable gas limit
133
134    // Send the transaction using the client directly
135    log::info!("Sending transaction to contract at {}", contract.address);
136    log::info!("Function: {}, Args: {:?}", function_name, args);
137
138    // Log detailed information about the transaction
139    log::debug!("Sending transaction to contract at {}", contract.address);
140    log::debug!("Function: {}, Args: {:?}", function_name, args);
141    log::debug!("From address: {}", wallet.address);
142    log::debug!("Gas limit: {:?}", tx.gas);
143
144    let pending_tx = match client.send_transaction(tx, None).await {
145        Ok(pending_tx) => {
146            log::debug!("Transaction sent successfully: {:?}", pending_tx.tx_hash());
147            log::info!("Transaction sent successfully: {:?}", pending_tx.tx_hash());
148            pending_tx
149        }
150        Err(e) => {
151            // Log the error for debugging
152            log::error!("Failed to send transaction: {}", e);
153            log::error!("ERROR DETAILS: {:?}", e);
154            return Err(CryptoError::ContractError(format!(
155                "Failed to send transaction: {}",
156                e
157            )));
158        }
159    };
160
161    // Return the transaction hash
162    Ok(pending_tx.tx_hash())
163}
164
165/// Estimates gas for a contract function call.
166pub async fn estimate_gas(
167    contract: &Contract,
168    wallet: &EthereumWallet,
169    provider: &Provider<Http>,
170    function_name: &str,
171    args: Vec<Token>,
172) -> Result<U256, CryptoError> {
173    // Get the function from the ABI
174    let function = contract
175        .abi
176        .function(function_name)
177        .map_err(|e| CryptoError::ContractError(format!("Function not found in ABI: {}", e)))?;
178
179    // Encode the function call
180    let call_data = function.encode_input(&args).map_err(|e| {
181        CryptoError::ContractError(format!("Failed to encode function call: {}", e))
182    })?;
183
184    // Create the transaction request
185    let tx = TransactionRequest::new()
186        .from(wallet.address)
187        .to(contract.address)
188        .data(call_data);
189
190    // Estimate gas
191    let gas = provider
192        .estimate_gas(&tx.into(), None)
193        .await
194        .map_err(|e| CryptoError::ContractError(format!("Failed to estimate gas: {}", e)))?;
195
196    Ok(gas)
197}