sal-vault 0.1.0

SAL Vault - Cryptographic functionality including key management, digital signatures, symmetric encryption, Ethereum wallets, and encrypted key-value store
Documentation
//! Smart contract interaction functionality.
//!
//! This module provides functionality for interacting with smart contracts on EVM-based blockchains.

use ethers::abi::{Abi, Token};
use ethers::prelude::*;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use std::sync::Arc;

use super::networks::NetworkConfig;
use super::wallet::EthereumWallet;
use crate::error::CryptoError;

/// A smart contract instance.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Contract {
    /// The contract address
    pub address: Address,
    /// The contract ABI
    pub abi: Abi,
    /// The network the contract is deployed on
    pub network: NetworkConfig,
}

impl Contract {
    /// Creates a new contract instance.
    pub fn new(address: Address, abi: Abi, network: NetworkConfig) -> Self {
        Contract {
            address,
            abi,
            network,
        }
    }

    /// Creates a new contract instance from an address string and ABI.
    pub fn from_address_string(
        address_str: &str,
        abi: Abi,
        network: NetworkConfig,
    ) -> Result<Self, CryptoError> {
        let address = Address::from_str(address_str)
            .map_err(|e| CryptoError::InvalidAddress(format!("Invalid address format: {}", e)))?;

        Ok(Contract::new(address, abi, network))
    }

    /// Creates an ethers Contract instance for interaction.
    pub fn create_ethers_contract(
        &self,
        provider: Provider<Http>,
        _wallet: Option<&EthereumWallet>,
    ) -> Result<ethers::contract::Contract<ethers::providers::Provider<Http>>, CryptoError> {
        let contract =
            ethers::contract::Contract::new(self.address, self.abi.clone(), Arc::new(provider));

        Ok(contract)
    }
}

/// Loads a contract ABI from a JSON string.
pub fn load_abi_from_json(json_str: &str) -> Result<Abi, CryptoError> {
    serde_json::from_str(json_str)
        .map_err(|e| CryptoError::SerializationError(format!("Failed to parse ABI JSON: {}", e)))
}

/// Calls a read-only function on a contract.
pub async fn call_read_function(
    contract: &Contract,
    provider: &Provider<Http>,
    function_name: &str,
    args: Vec<Token>,
) -> Result<Vec<Token>, CryptoError> {
    // Create the ethers contract (not used directly but kept for future extensions)
    let _ethers_contract = contract.create_ethers_contract(provider.clone(), None)?;

    // Get the function from the ABI
    let function = contract
        .abi
        .function(function_name)
        .map_err(|e| CryptoError::ContractError(format!("Function not found in ABI: {}", e)))?;

    // Encode the function call
    let call_data = function.encode_input(&args).map_err(|e| {
        CryptoError::ContractError(format!("Failed to encode function call: {}", e))
    })?;

    // Make the call
    let tx = TransactionRequest::new()
        .to(contract.address)
        .data(call_data);

    let result = provider
        .call(&tx.into(), None)
        .await
        .map_err(|e| CryptoError::ContractError(format!("Contract call failed: {}", e)))?;

    // Decode the result
    let decoded = function.decode_output(&result).map_err(|e| {
        CryptoError::ContractError(format!("Failed to decode function output: {}", e))
    })?;

    Ok(decoded)
}

/// Executes a state-changing function on a contract.
pub async fn call_write_function(
    contract: &Contract,
    wallet: &EthereumWallet,
    provider: &Provider<Http>,
    function_name: &str,
    args: Vec<Token>,
) -> Result<H256, CryptoError> {
    // Create a client with the wallet
    let client = SignerMiddleware::new(provider.clone(), wallet.wallet.clone());

    // Get the function from the ABI
    let function = contract
        .abi
        .function(function_name)
        .map_err(|e| CryptoError::ContractError(format!("Function not found in ABI: {}", e)))?;

    // Encode the function call
    let call_data = function.encode_input(&args).map_err(|e| {
        CryptoError::ContractError(format!("Failed to encode function call: {}", e))
    })?;

    // Create the transaction request with gas limit
    let tx = TransactionRequest::new()
        .to(contract.address)
        .data(call_data)
        .gas(U256::from(300000)); // Set a reasonable gas limit

    // Send the transaction using the client directly
    log::info!("Sending transaction to contract at {}", contract.address);
    log::info!("Function: {}, Args: {:?}", function_name, args);

    // Log detailed information about the transaction
    log::debug!("Sending transaction to contract at {}", contract.address);
    log::debug!("Function: {}, Args: {:?}", function_name, args);
    log::debug!("From address: {}", wallet.address);
    log::debug!("Gas limit: {:?}", tx.gas);

    let pending_tx = match client.send_transaction(tx, None).await {
        Ok(pending_tx) => {
            log::debug!("Transaction sent successfully: {:?}", pending_tx.tx_hash());
            log::info!("Transaction sent successfully: {:?}", pending_tx.tx_hash());
            pending_tx
        }
        Err(e) => {
            // Log the error for debugging
            log::error!("Failed to send transaction: {}", e);
            log::error!("ERROR DETAILS: {:?}", e);
            return Err(CryptoError::ContractError(format!(
                "Failed to send transaction: {}",
                e
            )));
        }
    };

    // Return the transaction hash
    Ok(pending_tx.tx_hash())
}

/// Estimates gas for a contract function call.
pub async fn estimate_gas(
    contract: &Contract,
    wallet: &EthereumWallet,
    provider: &Provider<Http>,
    function_name: &str,
    args: Vec<Token>,
) -> Result<U256, CryptoError> {
    // Get the function from the ABI
    let function = contract
        .abi
        .function(function_name)
        .map_err(|e| CryptoError::ContractError(format!("Function not found in ABI: {}", e)))?;

    // Encode the function call
    let call_data = function.encode_input(&args).map_err(|e| {
        CryptoError::ContractError(format!("Failed to encode function call: {}", e))
    })?;

    // Create the transaction request
    let tx = TransactionRequest::new()
        .from(wallet.address)
        .to(contract.address)
        .data(call_data);

    // Estimate gas
    let gas = provider
        .estimate_gas(&tx.into(), None)
        .await
        .map_err(|e| CryptoError::ContractError(format!("Failed to estimate gas: {}", e)))?;

    Ok(gas)
}