odra-cli 2.6.0

Odra CLI - Command Line Interface for Odra smart contracts.
Documentation
use std::{fs::File, io::Write, path::PathBuf, str::FromStr};

use chrono::{SecondsFormat, Utc};
use odra::{
    contract_def::HasIdent,
    host::{HostEnv, HostRef, HostRefLoader},
    prelude::{Address, Addressable},
    OdraContract
};
use serde_derive::{Deserialize, Serialize};
use thiserror::Error;

use crate::{
    cmd::args::{DEPLOY_MODE_ARCHIVE, DEPLOY_MODE_OVERRIDE},
    log,
    utils::get_default_contracts_file
};

#[derive(Error, Debug)]
pub enum ContractError {
    #[error("TOML serialization error")]
    TomlSerialize(#[from] toml::ser::Error),
    #[error("TOML deserialization error")]
    TomlDeserialize(#[from] toml::de::Error),
    #[error("Couldn't read file")]
    Io(#[from] std::io::Error),
    #[error("Couldn't find contract `{0}`")]
    NotFound(String),
    #[error("Couldn't find schema file for contract `{0}`")]
    SchemaFileNotFound(String),
    #[error("Contract `{0}` already exists")]
    ContractExists(String)
}

/// Represents storage for deployed contracts.
/// This trait defines the methods for reading and writing contract data.
pub(crate) trait ContractStorage {
    /// Reads the contract data from the storage.
    fn read(&self) -> Result<ContractsData, ContractError>;
    /// Writes the contract data to the storage.
    fn write(&mut self, data: &ContractsData) -> Result<(), ContractError>;
    /// Creates a backup copy of the contract data.
    fn backup(&self) -> Result<(), ContractError>;
}

/// Represents the data structure for storing deployed contracts in a TOML file.
pub(crate) struct FileContractStorage {
    file_path: PathBuf
}

impl FileContractStorage {
    pub fn new(custom_path: Option<PathBuf>) -> Result<Self, ContractError> {
        let mut path = project_root::get_project_root().map_err(ContractError::Io)?;
        match &custom_path {
            Some(path_str) if !path_str.to_str().unwrap_or_default().is_empty() => {
                path.push(path_str);
            }
            _ => {
                let default_file = get_default_contracts_file();
                path.push(&default_file);
            }
        }
        if !path.exists() {
            let parent_path = path.parent().ok_or_else(|| {
                ContractError::Io(std::io::Error::new(
                    std::io::ErrorKind::NotFound,
                    "Parent directory not found"
                ))
            })?;
            std::fs::create_dir_all(parent_path).map_err(ContractError::Io)?;
        }

        Ok(Self { file_path: path })
    }
}

impl ContractStorage for FileContractStorage {
    fn read(&self) -> Result<ContractsData, ContractError> {
        let file = std::fs::read_to_string(&self.file_path).map_err(ContractError::Io)?;
        toml::from_str(&file).map_err(ContractError::TomlDeserialize)
    }

    fn write(&mut self, data: &ContractsData) -> Result<(), ContractError> {
        let content = toml::to_string_pretty(&data).map_err(ContractError::TomlSerialize)?;
        let mut file = File::create(&self.file_path).map_err(ContractError::Io)?;
        file.write_all(content.as_bytes())
            .map_err(ContractError::Io)?;
        Ok(())
    }

    fn backup(&self) -> Result<(), ContractError> {
        let mut new_path = self.file_path.with_extension("old");
        while new_path.exists() {
            new_path = new_path.with_added_extension("old");
        }
        std::fs::copy(&self.file_path, new_path).map_err(ContractError::Io)?;
        Ok(())
    }
}

/// This trait defines the methods for providing access to deployed contracts.
pub trait ContractProvider {
    /// Gets a reference to the contract.
    ///
    /// Returns a reference to the contract if it is found, otherwise returns an error.
    fn contract_ref<T: OdraContract + 'static>(
        &self,
        env: &HostEnv
    ) -> Result<T::HostRef, ContractError>;

    /// Gets a reference to the named contract.
    ///
    /// Returns a reference to the contract if it is found, otherwise returns an error.
    fn contract_ref_named<T: OdraContract + 'static>(
        &self,
        env: &HostEnv,
        name: Option<String>
    ) -> Result<T::HostRef, ContractError>;

    /// Returns a list of all deployed contracts with their names and addresses.
    fn all_contracts(&self) -> Vec<DeployedContract>;

    /// Returns the contract address.
    fn address_by_name(&self, name: &str) -> Option<Address>;
}

/// Struct representing the deployed contracts.
///
/// This struct is used to store the contracts name and address at the deploy
/// time and to retrieve a reference to the contract at runtime.
///
/// The data is stored in a TOML file `deployed_contracts.toml` in the
/// `{project_root}/resources` directory.
pub struct DeployedContractsContainer {
    data: std::cell::RefCell<ContractsData>,
    storage: std::cell::RefCell<Box<dyn ContractStorage>>
}

impl DeployedContractsContainer {
    /// Creates a new instance.
    pub(crate) fn instance(storage: impl ContractStorage + 'static) -> Self {
        match storage.read() {
            Ok(data) => Self {
                data: std::cell::RefCell::new(data),
                storage: std::cell::RefCell::new(Box::new(storage))
            },
            Err(_) => Self {
                data: std::cell::RefCell::new(Default::default()),
                storage: std::cell::RefCell::new(Box::new(storage))
            }
        }
    }

    pub fn apply_deploy_mode(&self, mode: String) -> Result<(), ContractError> {
        match mode.as_str() {
            DEPLOY_MODE_OVERRIDE => {
                self.data.borrow_mut().contracts.clear();
                let data = self.data.borrow();
                let mut storage = self.storage.borrow_mut();
                storage.write(&data)?;
                log("Contracts configuration has been overridden");
            }
            DEPLOY_MODE_ARCHIVE => {
                let storage = self.storage.borrow_mut();
                storage.backup()?;
                self.data.borrow_mut().contracts.clear();
                let data = self.data.borrow();
                let mut storage = self.storage.borrow_mut();
                storage.write(&data)?;
                log("Starting fresh deployment. Previous contracts configuration has been backed up.");
            }
            _ => {} // default mode does nothing
        }
        Ok(())
    }

    /// Adds a contract to the container.
    pub fn add_contract_named<T: HostRef + HasIdent>(
        &self,
        contract: &T,
        package_name: Option<String>
    ) -> Result<(), ContractError> {
        // Try to add the contract - will fail if package_name already exists
        self.data
            .borrow_mut()
            .add_contract::<T>(contract.address(), package_name)?;

        // Save to storage
        let data = self.data.borrow();
        let mut storage = self.storage.borrow_mut();
        storage.write(&data)
    }

    /// Adds a contract to the container.
    pub fn add_contract<T: HostRef + HasIdent>(&self, contract: &T) -> Result<(), ContractError> {
        self.add_contract_named(contract, None)
    }
}

impl ContractProvider for DeployedContractsContainer {
    fn contract_ref<T: OdraContract + 'static>(
        &self,
        env: &HostEnv
    ) -> Result<T::HostRef, ContractError> {
        self.contract_ref_named::<T>(env, None)
    }

    fn contract_ref_named<T: OdraContract + 'static>(
        &self,
        env: &HostEnv,
        package_name: Option<String>
    ) -> Result<T::HostRef, ContractError> {
        let name = package_name.unwrap_or(T::HostRef::ident());
        self.data
            .borrow()
            .contracts()
            .iter()
            .find(|c| c.key_name() == name)
            .map(|c| Address::from_str(&c.package_hash).ok())
            .and_then(|opt| opt.map(|addr| <T as HostRefLoader<T::HostRef>>::load(env, addr)))
            .ok_or(ContractError::NotFound(T::HostRef::ident()))
    }

    fn all_contracts(&self) -> Vec<DeployedContract> {
        self.data.borrow().contracts().clone()
    }

    fn address_by_name(&self, package_name: &str) -> Option<Address> {
        self.data
            .borrow()
            .contracts()
            .iter()
            .find(|c| c.key_name() == package_name)
            .and_then(|c| Address::from_str(&c.package_hash).ok())
    }
}

/// This struct represents a contract in the `deployed_contracts.toml` file.
#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct DeployedContract {
    name: String,
    #[serde(default)]
    package_name: String,
    package_hash: String
}

impl DeployedContract {
    fn new<T: HasIdent>(address: Address, name: Option<String>) -> Self {
        let contract_name = name.unwrap_or_else(|| T::ident());
        Self {
            name: T::ident(),
            package_name: contract_name,
            package_hash: address.to_string()
        }
    }

    pub fn key_name(&self) -> String {
        if self.package_name.is_empty() {
            self.name.clone()
        } else {
            self.package_name.clone()
        }
    }

    pub fn name(&self) -> String {
        self.name.clone()
    }

    pub fn address(&self) -> Address {
        Address::from_str(&self.package_hash).unwrap()
    }
}

#[derive(Deserialize, Serialize, Debug, Clone)]
pub(crate) struct ContractsData {
    #[serde(alias = "time")]
    last_updated: String,
    contracts: Vec<DeployedContract>
}

impl Default for ContractsData {
    fn default() -> Self {
        Self {
            last_updated: Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true),
            contracts: Vec::new()
        }
    }
}

impl ContractsData {
    pub fn add_contract<T: HasIdent>(
        &mut self,
        address: Address,
        package_name: Option<String>
    ) -> Result<(), ContractError> {
        let contract = DeployedContract::new::<T>(address, package_name);

        // Check if a contract with this package_name already exists
        if self
            .contracts
            .iter()
            .any(|c| c.package_name == contract.package_name)
        {
            return Err(ContractError::ContractExists(contract.package_name));
        }

        self.contracts.push(contract);
        self.last_updated = Utc::now().to_rfc3339_opts(SecondsFormat::Secs, true);
        Ok(())
    }

    fn contracts(&self) -> &Vec<DeployedContract> {
        &self.contracts
    }
}