multiversx-chain-vm 0.23.1

MultiversX VM implementation and tooling
Documentation
use std::fmt;

use multiversx_chain_core::types::ReturnCode;

use crate::{
    host::context::{TxFunctionName, TxInput},
    vm_err_msg,
};

use super::{AsyncCallTxData, GasUsed, TxErrorTrace, TxLog, TxPanic, TxResultCalls};

#[derive(Clone, Debug)]
#[must_use]
pub struct TxResult {
    pub result_status: ReturnCode,
    pub result_message: String,
    pub result_values: Vec<Vec<u8>>,

    /// Contains a log generated by ESDT transfer builtin functions,
    /// like `MultiESDTNFTTransfer` or `ESDTTransfer`.
    ///
    /// There is at most one such log per transaction, hence Option, not Vec.
    ///
    /// It is kept separately, because, unlike regular logs,
    /// it is not erased in case of tx failure (e.g. via sync call fallbile).
    ///
    /// It also appears in front of the regular logs, it is easier to merge this way.
    ///
    /// It does get merged into the regular logs when merging results after a sync call.
    pub esdt_transfer_log: Option<TxLog>,

    /// Logs created during the transaction execution.
    pub result_logs: Vec<TxLog>,

    pub gas_used: GasUsed,

    /// Accumulates errors when they occur.
    ///
    /// It mimics the behavior of the Go VM,
    /// and they contribute to the internalVMErrors event log.
    ///
    /// Of course, the data field of the internalVMErrors log is implementation-dependent,
    /// so a 1:1 match is impossible.
    pub error_trace: Vec<TxErrorTrace>,

    /// Calls that need to be executed.
    ///
    /// Structure is emptied as soon as async calls are executed.
    pub pending_calls: TxResultCalls,

    /// All async calls launched from the tx (legacy async, promises, transfer-execute).
    ///
    /// Is never cleared of its contents.
    pub all_calls: Vec<AsyncCallTxData>,
}

impl Default for TxResult {
    fn default() -> Self {
        TxResult {
            result_status: ReturnCode::Success,
            result_message: String::new(),
            result_values: Vec::new(),
            esdt_transfer_log: None,
            result_logs: Vec::new(),
            gas_used: GasUsed::Unknown,
            error_trace: Vec::new(),
            pending_calls: TxResultCalls::empty(),
            all_calls: Vec::new(),
        }
    }
}

impl TxResult {
    pub fn empty() -> TxResult {
        TxResult::default()
    }

    pub fn print(&self) {
        println!("{self}");
    }

    pub fn from_panic_obj(panic_obj: &TxPanic) -> Self {
        TxResult::from_error(panic_obj.status, &panic_obj.message)
    }

    pub fn from_panic_string(s: &str) -> Self {
        TxResult::from_error(ReturnCode::UserError, s)
    }

    pub fn from_unknown_panic() -> Self {
        Self::from_panic_string("")
    }

    pub fn from_error<S>(return_code: ReturnCode, result_message: S) -> Self
    where
        S: Into<String>,
    {
        TxResult {
            result_status: return_code,
            result_message: result_message.into(),
            ..Default::default()
        }
    }

    pub fn from_vm_error<S>(result_message: S) -> Self
    where
        S: Into<String>,
    {
        TxResult::from_error(ReturnCode::ExecutionFailed, result_message)
    }

    pub fn from_function_not_found() -> Self {
        TxResult::from_error(ReturnCode::FunctionNotFound, vm_err_msg::FUNCTION_NOT_FOUND)
    }

    /// Retrieves all logs, including the ESDT transfer log if present.
    pub fn all_logs(&self) -> Vec<&TxLog> {
        let mut all_logs = Vec::new();
        if let Some(esdt_log) = &self.esdt_transfer_log {
            all_logs.push(esdt_log);
        }
        for log in &self.result_logs {
            all_logs.push(log);
        }
        all_logs
    }

    /// Just like Vec::append, consumes the source TxResult logs.
    pub fn append_all_logs(&mut self, source: &mut TxResult) {
        if let Some(esdt_log) = source.esdt_transfer_log.take() {
            self.result_logs.push(esdt_log);
        }
        self.result_logs.append(&mut source.result_logs);
    }

    pub fn merge_after_sync_call(&mut self, sync_call_result: &TxResult) {
        self.result_values
            .extend_from_slice(&sync_call_result.result_values);
        if let Some(transfer_log) = &sync_call_result.esdt_transfer_log {
            self.result_logs.push(transfer_log.clone());
        }
        self.result_logs
            .extend_from_slice(&sync_call_result.result_logs);
        self.error_trace
            .extend_from_slice(&sync_call_result.error_trace);
        if let Some(sync_result_async) = &sync_call_result.pending_calls.async_call {
            assert!(
                self.pending_calls.async_call.is_none(),
                "Multiple async calls not supported"
            );
            self.pending_calls.async_call = Some(sync_result_async.clone());
        }
    }

    /// Clears all TxResult fields, and replaces the error status and message fields.
    ///
    /// It also concatenates the error traces. In practice, the error_tx_result contains no error trace.
    ///
    /// Implementation note: this is done via `std::mem::replace` to ensure that all fields are cleared, except the ones related to errors.
    pub fn merge_error(&mut self, error_tx_result: TxResult) {
        let mut old_value = std::mem::replace(self, error_tx_result);

        if let Some(transfer_log) = old_value.esdt_transfer_log {
            self.result_logs.push(transfer_log);
        }
        self.error_trace.append(&mut old_value.error_trace);
    }

    pub fn assert_ok(&self) {
        assert!(
            self.result_status.is_success(),
            "Tx success expected, but failed. Status: {}, message: \"{}\"",
            self.result_status,
            self.result_message.as_str()
        );
    }

    pub fn assert_error(&self, expected_status: u64, expected_message: &str) {
        assert!(
            self.result_message.as_str() == expected_message,
            "Tx error message mismatch. Want status {}, message \"{}\". Have status {}, message \"{}\"",
            expected_status,
            expected_message,
            self.result_status,
            self.result_message.as_str()
        );
        assert!(
            self.result_status.as_u64() == expected_status,
            "Tx error status mismatch. Want status {}, message \"{}\". Have status {}, message \"{}\"",
            expected_status,
            expected_message,
            self.result_status,
            self.result_message.as_str()
        );
    }

    pub fn assert_user_error(&self, expected_message: &str) {
        self.assert_error(ReturnCode::UserError.as_u64(), expected_message);
    }
}

impl fmt::Display for TxResult {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        let results_hex: Vec<String> = self
            .result_values
            .iter()
            .map(|r| format!("0x{}", hex::encode(r)))
            .collect();
        write!(
            f,
            "TxResult {{\n\tresult_status: {},\n\tresult_values:{results_hex:?}\n}}",
            self.result_status
        )
    }
}

impl TxResult {
    pub fn result_values_to_string(&self) -> String {
        result_values_to_string(&self.result_values)
    }
}

pub fn result_values_to_string(values: &[Vec<u8>]) -> String {
    itertools::join(
        values.iter().map(|val| format!("0x{}", hex::encode(val))),
        ", ",
    )
}

impl TxResult {
    pub fn append_internal_vm_errors_event_log(&mut self, input: &TxInput) {
        if self.error_trace.is_empty() {
            return;
        }

        self.result_logs.push(TxLog {
            address: input.from.clone(),
            endpoint: TxFunctionName::from_static("internalVMErrors"),
            topics: vec![input.to.to_vec(), input.func_name.to_bytes()],
            data: self
                .error_trace
                .iter()
                .map(|err| err.error_trace_message.as_bytes().to_vec())
                .collect(),
        });
    }
}