odra-cli 2.10.1

Odra CLI - Command Line Interface for Odra smart contracts.
#![allow(dead_code)]
use clap::ArgMatches;
use odra::prelude::{Address, OdraError};
use odra::schema::{
    casper_contract_schema::{Entrypoint, NamedCLType},
    codec::CustomTypeSet
};
use odra::VmError;
use odra::{casper_types::U512, host::HostEnv, CallDef};

use crate::cmd::args::{read_arg, Arg, ArgsError, ARG_PRINT_EVENTS};
use crate::container;
use crate::container::ContractProvider;

pub(crate) mod cmd_args;
pub(crate) mod factory;
pub(crate) mod runtime_args;
mod utils;

#[derive(Debug, thiserror::Error)]
pub enum CallError {
    #[error("Calling {package_name}::{method} failed with: {message}")]
    ExecutionError {
        package_name: String,
        method: String,
        message: String
    },
    #[error(transparent)]
    ArgsError(#[from] ArgsError),
    #[error(transparent)]
    TypesError(#[from] odra_schema::codec::Error),
    #[error("Contract not found")]
    ContractNotFound,
    #[error(transparent)]
    ContractError(#[from] container::ContractError),
    #[error("Entry point '{entry_point}' not found in contract '{contract_name}'")]
    EntryPointNotFound {
        entry_point: String,
        contract_name: String
    },
    #[error("No entry point found in contract '{contract_name}'")]
    NoEntryPointFound { contract_name: String },
    #[error("Invalid gas value: {0}")]
    InvalidGasValue(String),
    #[error("Decoding error: {0}")]
    DecodingError(#[from] odra_schema::codec::DecodeError),
    #[error("Invalid arg value: {0:?}")]
    CLValueError(odra::casper_types::CLValueError)
}

/// The outcome of a contract call: the decoded return value plus any events captured when
/// `--print-events` was set on a mutable call. Building this instead of printing inline keeps
/// `--json` output clean — rendering is left entirely to the command layer.
pub(crate) struct CallOutcome {
    /// The decoded return value, or an empty string when the entry point returns nothing.
    pub result: String,
    /// Events captured during the call, grouped by contract. Empty unless `--print-events` was set.
    pub events: Vec<ContractEvents>
}

/// Events emitted by a single contract during a `--print-events` mutable call.
#[derive(serde_derive::Serialize)]
pub(crate) struct ContractEvents {
    pub contract: String,
    pub events: Vec<String>
}

/// Calls `entry_point` of the contract `contract_name`. For a factory (`is_factory`) the call
/// carries the `odra_cfg_*` arguments its generated ref would add.
pub fn call<T: ContractProvider>(
    env: &HostEnv,
    contract_name: &str,
    entry_point: &Entrypoint,
    is_factory: bool,
    args: &ArgMatches,
    types: &CustomTypeSet,
    contract_provider: &T
) -> Result<CallOutcome, CallError> {
    let amount = read_arg::<U512>(args, Arg::AttachedValue).unwrap_or_default();

    let mut runtime_args = runtime_args::compose(entry_point, args, types)?;
    if is_factory {
        factory::insert_cfg_args(&entry_point.name, &mut runtime_args)
            .map_err(CallError::CLValueError)?;
    }
    let contract_address = contract_provider
        .address_by_name(contract_name)
        .ok_or(CallError::ContractNotFound)?;

    let method = &entry_point.name;
    let is_mut = entry_point.is_mutable;
    let ty = &entry_point.return_ty;
    let call_def = CallDef::new(method, is_mut, runtime_args).with_amount(amount);
    let use_proxy = ty.0 != NamedCLType::Unit || !call_def.amount().is_zero();

    if is_mut {
        let gas = read_arg(args, Arg::Gas).ok_or(CallError::InvalidGasValue(
            "Failed to read gas value. Use --gas <value> to specify it.".to_string()
        ))?;
        env.set_gas(gas);
    }

    let print_events = is_mut && args.get_flag(ARG_PRINT_EVENTS);
    env.set_captures_events(print_events);
    let bytes = env
        .raw_call_contract(contract_address, call_def, use_proxy)
        .map_err(|e| CallError::ExecutionError {
            package_name: contract_name.to_string(),
            method: method.to_string(),
            message: match e {
                OdraError::VmError(VmError::Other(msg)) => msg,
                _ => format!("{:?}", e)
            }
        })?;

    let events = if print_events {
        collect_events(env, contract_provider, types, contract_address)?
    } else {
        Vec::new()
    };

    let result = odra_schema::codec::decode(bytes.inner_bytes(), ty, types)?;
    Ok(CallOutcome {
        result: result.0,
        events
    })
}

/// Collects and decodes the events captured during the last call, grouped by contract.
fn collect_events<T: ContractProvider>(
    env: &HostEnv,
    contract_provider: &T,
    types: &CustomTypeSet,
    contract_address: Address
) -> Result<Vec<ContractEvents>, CallError> {
    let call_result = env.last_call_result(contract_address).raw_call_result();

    let mut grouped = Vec::new();
    for deployed_contract in contract_provider.all_contracts() {
        let events = call_result.contract_events(&deployed_contract.address());
        if events.is_empty() {
            continue;
        }
        let decoded = events
            .iter()
            .map(|event| odra_schema::codec::decode_event(event, types))
            .collect::<Result<Vec<_>, _>>()?;
        grouped.push(ContractEvents {
            contract: deployed_contract.key_name(),
            events: decoded
        });
    }
    Ok(grouped)
}