Skip to main content

mega_evme/common/
outcome.rs

1//! Execution outcome and output formatting for mega-evme commands
2
3use std::{path::Path, time::Duration};
4
5use super::{EvmeError, StateDumpArgs, TraceArgs};
6
7use alloy_consensus::{Eip658Value, Receipt};
8use alloy_primitives::{hex, Address, BlockHash, Bytes, TxHash, B256};
9use alloy_rpc_types_eth::TransactionReceipt;
10use alloy_sol_types::{Panic, Revert, SolError};
11use clap::Parser;
12use mega_evm::{
13    op_revm::OpHaltReason,
14    revm::{context::result::ExecutionResult, state::EvmState},
15    MegaHaltReason, MegaTxType,
16};
17use op_alloy_consensus::{OpDepositReceipt, OpReceiptEnvelope};
18use serde::Serialize;
19
20/// OP-stack transaction receipt type alias
21pub type OpTxReceipt = TransactionReceipt<OpReceiptEnvelope<alloy_rpc_types_eth::Log>>;
22
23/// Common execution outcome for all evme commands
24#[derive(Debug)]
25pub struct EvmeOutcome {
26    /// The nonce of the sender before execution
27    pub pre_execution_nonce: u64,
28    /// The EVM execution result
29    pub exec_result: ExecutionResult<mega_evm::MegaHaltReason>,
30    /// The post-execution EVM state
31    pub state: EvmState,
32    /// Time taken to execute
33    pub exec_time: Duration,
34    /// Optional trace data (if tracing was enabled)
35    pub trace_data: Option<String>,
36}
37
38impl EvmeOutcome {
39    /// Convert the execution outcome to an OP receipt envelope.
40    ///
41    /// For deposit transactions (type 126), provide `deposit_nonce` and optionally
42    /// `deposit_receipt_version` (introduced in Canyon hardfork).
43    pub fn to_op_receipt(&self, tx_type: MegaTxType, state_nonce: u64) -> OpReceiptEnvelope {
44        // Build base receipt
45        let receipt = Receipt {
46            status: Eip658Value::Eip658(self.exec_result.is_success()),
47            cumulative_gas_used: self.exec_result.gas_used(),
48            logs: self.exec_result.logs().to_vec(),
49        };
50
51        // Wrap in OpReceiptEnvelope based on transaction type
52        match tx_type {
53            MegaTxType::Legacy => OpReceiptEnvelope::Legacy(receipt.with_bloom()),
54            MegaTxType::Eip2930 => OpReceiptEnvelope::Eip2930(receipt.with_bloom()),
55            MegaTxType::Eip1559 => OpReceiptEnvelope::Eip1559(receipt.with_bloom()),
56            MegaTxType::Eip7702 => OpReceiptEnvelope::Eip7702(receipt.with_bloom()),
57            MegaTxType::Deposit => {
58                let deposit_receipt = OpDepositReceipt {
59                    inner: receipt,
60                    deposit_nonce: Some(state_nonce),
61                    deposit_receipt_version: Some(1),
62                };
63                OpReceiptEnvelope::Deposit(deposit_receipt.with_bloom())
64            }
65        }
66    }
67}
68
69/// Convert an [`OpReceiptEnvelope`] to an OP transaction receipt.
70#[allow(clippy::too_many_arguments)]
71pub fn op_receipt_to_tx_receipt(
72    receipt: &OpReceiptEnvelope,
73    block_number: u64,
74    block_timestamp: u64,
75    from: Address,
76    to: Option<Address>,
77    contract_address: Option<Address>,
78    effective_gas_price: u128,
79    gas_used: u64,
80    transaction_hash: Option<TxHash>, // only used for replay command where tx hash is known
81    block_hash: Option<BlockHash>,    // only used for replay command where block hash is known
82    transaction_index: u64,
83) -> OpTxReceipt {
84    // Map logs to include block/tx metadata
85    let mut log_index = 0;
86    let inner = receipt.clone().map_logs(|log| {
87        let log = alloy_rpc_types_eth::Log {
88            inner: log,
89            block_hash: None,
90            block_number: Some(block_number),
91            block_timestamp: Some(block_timestamp),
92            transaction_hash: Some(B256::ZERO),
93            transaction_index: Some(0),
94            log_index: Some(log_index),
95            removed: false,
96        };
97        log_index += 1;
98        log
99    });
100
101    TransactionReceipt {
102        inner,
103        transaction_hash: transaction_hash.unwrap_or_default(),
104        transaction_index: Some(transaction_index),
105        block_hash,
106        block_number: Some(block_number),
107        gas_used,
108        effective_gas_price,
109        blob_gas_used: None,
110        blob_gas_price: None,
111        from,
112        to,
113        contract_address,
114    }
115}
116
117/// Print a human-readable execution summary.
118pub fn print_execution_summary(
119    exec_result: &ExecutionResult<MegaHaltReason>,
120    contract_address: Option<Address>,
121    exec_time: Duration,
122) {
123    println!();
124    println!("=== Transaction Summary ===");
125
126    match exec_result {
127        ExecutionResult::Success { gas_used, logs, output, .. } => {
128            println!("Status:           Success");
129            println!("Gas Used:         {}", gas_used);
130            println!("Execution Time:   {:?}", exec_time);
131            if let Some(addr) = contract_address {
132                println!("Contract Address: {}", addr);
133            }
134            if !logs.is_empty() {
135                println!("Events:           {} log(s) emitted", logs.len());
136            }
137            let output_data = output.data();
138            if !output_data.is_empty() {
139                println!("Output:           0x{}", hex::encode(output_data));
140            }
141        }
142        ExecutionResult::Revert { gas_used, output } => {
143            println!("Status:           Reverted");
144            println!("Gas Used:         {}", gas_used);
145            println!("Execution Time:   {:?}", exec_time);
146            println!("Revert Reason:    {}", decode_revert_reason(output));
147        }
148        ExecutionResult::Halt { gas_used, reason } => {
149            println!("Status:           Halted");
150            println!("Gas Used:         {}", gas_used);
151            println!("Execution Time:   {:?}", exec_time);
152            println!("Halt Reason:      {}", format_halt_reason(reason));
153        }
154    }
155}
156
157/// Decode revert reason from output bytes using alloy's built-in decoders.
158///
159/// Supports:
160/// - `Error(string)` via `alloy_sol_types::Revert`
161/// - `Panic(uint256)` via `alloy_sol_types::Panic`
162/// - Raw hex fallback
163fn decode_revert_reason(output: &Bytes) -> String {
164    if output.is_empty() {
165        return "(empty)".to_string();
166    }
167
168    // Try to decode as Revert (Error(string))
169    if let Ok(revert) = Revert::abi_decode(output) {
170        return format!("Error(\"{}\")", revert.reason());
171    }
172
173    // Try to decode as Panic (Panic(uint256))
174    if let Ok(panic) = Panic::abi_decode(output) {
175        return if let Some(kind) = panic.kind() {
176            format!("Panic: {}", kind)
177        } else {
178            format!("Panic(0x{:x})", panic.code)
179        };
180    }
181
182    // Fallback: raw hex
183    format!("0x{}", hex::encode(output))
184}
185
186/// Format halt reason for display.
187fn format_halt_reason(reason: &MegaHaltReason) -> String {
188    match reason {
189        MegaHaltReason::Base(op_reason) => format_op_halt_reason(op_reason),
190        _ => format!("{:?}", reason),
191    }
192}
193
194/// Format OP halt reason for display.
195fn format_op_halt_reason(reason: &OpHaltReason) -> String {
196    match reason {
197        OpHaltReason::Base(eth_reason) => format!("{:?}", eth_reason),
198        _ => format!("{:?}", reason),
199    }
200}
201
202/// Print a receipt as pretty-printed JSON.
203pub fn print_receipt<T: serde::Serialize>(receipt: &T) {
204    println!();
205    println!("=== Receipt ===");
206    match serde_json::to_string_pretty(receipt) {
207        Ok(json) => println!("{}", json),
208        Err(e) => println!("Failed to serialize receipt: {}", e),
209    }
210}
211
212/// Print execution trace to console or write to file.
213///
214/// If `output_file` is provided, writes the trace to the file and prints the path.
215/// Otherwise, prints the trace to the console.
216pub fn print_execution_trace(
217    trace: Option<&str>,
218    output_file: Option<&Path>,
219) -> Result<(), EvmeError> {
220    let Some(trace) = trace else {
221        return Ok(());
222    };
223
224    println!();
225    println!("=== Execution Trace ===");
226
227    if let Some(path) = output_file {
228        std::fs::write(path, trace)
229            .map_err(|e| EvmeError::Other(format!("Failed to write trace to file: {}", e)))?;
230        println!("Trace written to: {}", path.display());
231    } else {
232        println!("{}", trace);
233    }
234
235    Ok(())
236}
237
238/// Output format configuration
239#[derive(Parser, Debug, Clone, Default)]
240#[command(next_help_heading = "Output Options")]
241pub struct OutputArgs {
242    /// Output results as JSON instead of human-readable text
243    #[arg(long)]
244    pub json: bool,
245}
246
247/// Serializable execution summary for JSON output
248#[derive(Debug, Default, Serialize)]
249pub struct ExecutionSummary {
250    /// Whether the execution succeeded
251    pub success: bool,
252    /// Gas consumed by the execution
253    pub gas_used: u64,
254    /// Hex-encoded return data (present only on success with non-empty output)
255    #[serde(skip_serializing_if = "Option::is_none")]
256    pub output: Option<String>,
257    /// Deployed contract address (present only for successful CREATE transactions)
258    #[serde(skip_serializing_if = "Option::is_none")]
259    pub contract_address: Option<Address>,
260    /// Number of log entries emitted
261    pub logs_count: usize,
262    /// Decoded revert reason (present only on revert)
263    #[serde(skip_serializing_if = "Option::is_none")]
264    pub revert_reason: Option<String>,
265    /// Halt reason (present only on halt)
266    #[serde(skip_serializing_if = "Option::is_none")]
267    pub halt_reason: Option<String>,
268    /// Execution trace (present only when --trace is enabled without --trace.output)
269    #[serde(skip_serializing_if = "Option::is_none")]
270    pub trace: Option<serde_json::Value>,
271    /// Post-execution state dump (present only when --dump is enabled without --dump.output)
272    #[serde(skip_serializing_if = "Option::is_none")]
273    pub state: Option<serde_json::Value>,
274    /// Transaction receipt (present only for `tx` command)
275    #[serde(skip_serializing_if = "Option::is_none")]
276    pub receipt: Option<serde_json::Value>,
277}
278
279impl ExecutionSummary {
280    /// Fill trace and state dump fields from the execution outcome.
281    ///
282    /// When an output file is specified, data is written to that file.
283    /// Otherwise, data is inlined into the corresponding JSON field.
284    pub fn fill_trace_and_dump(
285        &mut self,
286        outcome: &EvmeOutcome,
287        trace_args: &TraceArgs,
288        dump_args: &StateDumpArgs,
289    ) -> Result<(), EvmeError> {
290        // Trace: inline or write to file
291        if let Some(trace) = outcome.trace_data.as_deref() {
292            if let Some(ref path) = trace_args.trace_output_file {
293                std::fs::write(path, trace).map_err(|e| {
294                    EvmeError::Other(format!("Failed to write trace to file: {}", e))
295                })?;
296            } else {
297                self.trace = Some(serde_json::from_str(trace).unwrap_or_else(|_| trace.into()));
298            }
299        }
300
301        // Dump: inline or write to file
302        if dump_args.dump {
303            let state_json = dump_args.serialize_evm_state(&outcome.state)?;
304            if let Some(ref path) = dump_args.dump_output_file {
305                std::fs::write(path, &state_json).map_err(|e| {
306                    EvmeError::Other(format!("Failed to write state dump to file: {}", e))
307                })?;
308            } else {
309                self.state =
310                    Some(serde_json::from_str(&state_json).unwrap_or_else(|_| state_json.into()));
311            }
312        }
313
314        Ok(())
315    }
316
317    /// Create from an `ExecutionResult` and optional contract address.
318    pub fn from_result(
319        exec_result: &ExecutionResult<MegaHaltReason>,
320        contract_address: Option<Address>,
321    ) -> Self {
322        match exec_result {
323            ExecutionResult::Success { gas_used, logs, output, .. } => {
324                let output_data = output.data();
325                Self {
326                    success: true,
327                    gas_used: *gas_used,
328                    output: if output_data.is_empty() {
329                        None
330                    } else {
331                        Some(format!("0x{}", hex::encode(output_data)))
332                    },
333                    contract_address,
334                    logs_count: logs.len(),
335                    ..Default::default()
336                }
337            }
338            ExecutionResult::Revert { gas_used, output } => Self {
339                gas_used: *gas_used,
340                revert_reason: Some(decode_revert_reason(output)),
341                ..Default::default()
342            },
343            ExecutionResult::Halt { gas_used, reason } => Self {
344                gas_used: *gas_used,
345                halt_reason: Some(format!("{:?}", reason)),
346                ..Default::default()
347            },
348        }
349    }
350}
351
352#[cfg(test)]
353mod tests {
354    use super::*;
355    use alloy_primitives::Bytes;
356    use alloy_sol_types::SolError;
357
358    #[test]
359    fn test_decode_revert_reason_empty() {
360        assert_eq!(decode_revert_reason(&Bytes::new()), "(empty)");
361    }
362
363    #[test]
364    fn test_decode_revert_reason_error_string() {
365        let encoded = Revert::from("insufficient balance").abi_encode();
366        assert_eq!(decode_revert_reason(&encoded.into()), "Error(\"insufficient balance\")");
367    }
368
369    #[test]
370    fn test_decode_revert_reason_panic() {
371        // Panic(0x01) = assert failure
372        let encoded = Panic { code: alloy_primitives::U256::from(0x01) }.abi_encode();
373        assert_eq!(decode_revert_reason(&encoded.into()), "Panic: assertion failed");
374    }
375
376    #[test]
377    fn test_decode_revert_reason_raw_hex() {
378        let raw = Bytes::from(vec![0xde, 0xad]);
379        assert_eq!(decode_revert_reason(&raw), "0xdead");
380    }
381}