cosm-orc 0.5.8

Cosmwasm smart contract orchestration and gas profiling library
Documentation
use anyhow::{Context, Result};
use log::{debug, info};
use serde::Serialize;
use serde_json::Value;
use std::ffi::OsStr;
use std::fmt::{self, Debug};
use std::fs;
use std::panic::Location;
use std::path::Path;

use crate::config::cfg::Config;
use crate::orchestrator::command::{exec_msg, CommandType};
use crate::orchestrator::deploy::ContractMap;
use crate::profilers::profiler::{Profiler, Report};

/// Stores cosmwasm contracts and executes their messages against the configured chain.
pub struct CosmOrc {
    pub contract_map: ContractMap,
    cfg: Config,
    profilers: Vec<Box<dyn Profiler + Send>>,
}

pub enum WasmMsg<X, Y, Z>
where
    X: Serialize,
    Y: Serialize,
    Z: Serialize,
{
    InstantiateMsg(X),
    ExecuteMsg(Y),
    QueryMsg(Z),
}

impl Debug for CosmOrc {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "{:?}", self.contract_map)
    }
}

impl CosmOrc {
    /// Creates a CosmOrc object from the supplied Config
    pub fn new(cfg: Config) -> Self {
        Self {
            contract_map: ContractMap::new(&cfg.code_ids),
            cfg,
            profilers: vec![],
        }
    }

    /// Used to add a profiler to be used during message execution.
    /// Call multiple times to add additional Profilers.
    pub fn add_profiler(mut self, p: Box<dyn Profiler + Send>) -> Self {
        self.profilers.push(p);
        self
    }

    // TODO: I probably shouldnt be returning `serde::Value`s from this library.
    // I should make it more general purpose and just return a byte slice of the json
    // for general consumption through any library, so they dont have to use serde.

    // TODO: Implement a `store_contract()` that takes in a single wasm file as well

    /// Uploads the contracts in `wasm_dir` to the configured chain
    /// saving the resulting contract ids in `contract_map` and
    /// returning the raw cosmos json responses.
    ///
    /// You don't need to call this function if all of the smart contract ids
    /// are already configured via `cfg.code_ids`.
    #[track_caller]
    pub fn store_contracts(&mut self, wasm_dir: &str) -> Result<Vec<Value>> {
        let caller_loc = Location::caller();
        let mut responses = vec![];
        let wasm_path = Path::new(wasm_dir);

        for wasm in fs::read_dir(wasm_path)? {
            let wasm_path = wasm?.path();
            if wasm_path.extension() == Some(OsStr::new("wasm")) {
                info!("Storing {:?}", wasm_path);

                let json = exec_msg(
                    &self.cfg.chain_cfg.binary,
                    CommandType::Store,
                    &[
                        vec![wasm_path
                            .to_str()
                            .context("invalid unicode chars")?
                            .to_string()],
                        self.cfg.tx_flags.clone(),
                    ]
                    .concat(),
                )?;

                let code_id: u64 = json["logs"][0]["events"][1]["attributes"][0]["value"]
                    .as_str()
                    .context("value is not a string")?
                    .parse()?;

                let contract = wasm_path
                    .file_stem()
                    .context("wasm_path has invalid filename")?
                    .to_str()
                    .context("wasm_path has invalid unicode chars")?
                    .to_string();

                self.contract_map
                    .register_contract(contract.clone(), code_id);

                for prof in &mut self.profilers {
                    prof.instrument(
                        contract.clone(),
                        "Store".to_string(),
                        CommandType::Store,
                        &json,
                        caller_loc,
                        0,
                    )?;
                }

                responses.push(json);
            }
        }
        Ok(responses)
    }

    /// Executes multiple smart contract operations against the configured chain
    /// returning the raw cosmos json responses.
    #[track_caller]
    pub fn process_msgs<X, Y, Z, S>(
        &mut self,
        contract_name: S,
        op_name: S,
        msgs: &[WasmMsg<X, Y, Z>],
    ) -> Result<Vec<Value>>
    where
        X: Serialize,
        Y: Serialize,
        Z: Serialize,
        S: Into<String>,
    {
        let caller_loc = Location::caller();
        let contract_name = contract_name.into();
        let op_name = op_name.into();

        let mut responses = vec![];
        for (idx, msg) in msgs.iter().enumerate() {
            let json = self.process_msg_internal(
                contract_name.clone(),
                op_name.clone(),
                msg,
                idx,
                caller_loc,
            )?;
            responses.push(json);
        }

        Ok(responses)
    }

    /// Executes a single smart contract operation against the configured chain
    /// returning the raw cosmos json response.
    /// # Arguments
    /// * `contract_name` - Smart contract name for the corresponding `msg`.
    /// * `op_name` - Human readable operation name for profiling bookkeeping usage.
    ///
    /// # Errors
    ///
    /// Returns [`Err`] if `contract_name` does not have a `DeployInfo` entry in `self.contract_map`.
    /// `contract_name` needs to be configured in `Config.code_ids`
    /// or `CosmOrc::store_contracts()` needs to be called with the `contract_name.wasm` in the passed directory.
    #[track_caller]
    pub fn process_msg<X, Y, Z, S>(
        &mut self,
        contract_name: S,
        op_name: S,
        msg: &WasmMsg<X, Y, Z>,
    ) -> Result<Value>
    where
        X: Serialize,
        Y: Serialize,
        Z: Serialize,
        S: Into<String>,
    {
        let caller_loc = Location::caller();
        self.process_msg_internal(contract_name.into(), op_name.into(), msg, 0, caller_loc)
    }

    // process_msg_internal is a private method with an index
    // of the passed in message for profiler bookkeeping
    fn process_msg_internal<X, Y, Z>(
        &mut self,
        contract_name: String,
        op_name: String,
        msg: &WasmMsg<X, Y, Z>,
        idx: usize,
        caller_loc: &Location,
    ) -> Result<Value>
    where
        X: Serialize,
        Y: Serialize,
        Z: Serialize,
    {
        let code_id = self.contract_map.code_id(&contract_name)?;

        let json = match msg {
            WasmMsg::InstantiateMsg(m) => {
                let input_json = serde_json::to_value(&m)?;

                let json = exec_msg(
                    &self.cfg.chain_cfg.binary,
                    CommandType::Instantiate,
                    &[
                        vec![
                            code_id.to_string(),
                            input_json.to_string(),
                            "--label".to_string(),
                            "gas profiler".to_string(),
                            "--no-admin".to_string(), // TODO: Allow for configurable admin addr to be passed
                        ],
                        self.cfg.tx_flags.clone(),
                    ]
                    .concat(),
                )?;

                for prof in &mut self.profilers {
                    prof.instrument(
                        contract_name.clone(),
                        op_name.clone(),
                        CommandType::Instantiate,
                        &json,
                        caller_loc,
                        idx,
                    )?;
                }

                let addr = json["logs"][0]["events"][0]["attributes"][0]["value"]
                    .as_str()
                    .context("not string")?
                    .to_string();

                self.contract_map.add_address(&contract_name, addr)?;

                json
            }
            WasmMsg::ExecuteMsg(m) => {
                let input_json = serde_json::to_value(&m)?;
                let addr = self.contract_map.address(&contract_name)?;

                let json = exec_msg(
                    &self.cfg.chain_cfg.binary,
                    CommandType::Execute,
                    &[
                        vec![addr, input_json.to_string()],
                        self.cfg.tx_flags.clone(),
                    ]
                    .concat(),
                )?;

                for prof in &mut self.profilers {
                    prof.instrument(
                        contract_name.clone(),
                        op_name.clone(),
                        CommandType::Execute,
                        &json,
                        caller_loc,
                        idx,
                    )?;
                }

                json
            }
            WasmMsg::QueryMsg(m) => {
                let input_json = serde_json::to_value(&m)?;
                let addr = self.contract_map.address(&contract_name)?;

                let json = exec_msg(
                    &self.cfg.chain_cfg.binary,
                    CommandType::Query,
                    &[
                        addr,
                        input_json.to_string(),
                        "--node".to_string(),
                        self.cfg.chain_cfg.rpc_endpoint.clone(),
                        "--output".to_string(),
                        "json".to_string(),
                    ],
                )?;

                for prof in &mut self.profilers {
                    prof.instrument(
                        contract_name.clone(),
                        op_name.clone(),
                        CommandType::Query,
                        &json,
                        caller_loc,
                        idx,
                    )?;
                }

                json
            }
        };

        debug!("{}", json);
        Ok(json)
    }

    /// Get instrumentation reports for each configured profiler.
    pub fn profiler_reports(&self) -> Result<Vec<Report>> {
        let mut reports = vec![];
        for prof in &self.profilers {
            reports.push(prof.report()?);
        }

        Ok(reports)
    }
}