1use 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
20pub type OpTxReceipt = TransactionReceipt<OpReceiptEnvelope<alloy_rpc_types_eth::Log>>;
22
23#[derive(Debug)]
25pub struct EvmeOutcome {
26 pub pre_execution_nonce: u64,
28 pub exec_result: ExecutionResult<mega_evm::MegaHaltReason>,
30 pub state: EvmState,
32 pub exec_time: Duration,
34 pub trace_data: Option<String>,
36}
37
38impl EvmeOutcome {
39 pub fn to_op_receipt(&self, tx_type: MegaTxType, state_nonce: u64) -> OpReceiptEnvelope {
44 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 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#[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>, block_hash: Option<BlockHash>, transaction_index: u64,
83) -> OpTxReceipt {
84 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
117pub 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
157fn decode_revert_reason(output: &Bytes) -> String {
164 if output.is_empty() {
165 return "(empty)".to_string();
166 }
167
168 if let Ok(revert) = Revert::abi_decode(output) {
170 return format!("Error(\"{}\")", revert.reason());
171 }
172
173 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 format!("0x{}", hex::encode(output))
184}
185
186fn 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
194fn format_op_halt_reason(reason: &OpHaltReason) -> String {
196 match reason {
197 OpHaltReason::Base(eth_reason) => format!("{:?}", eth_reason),
198 _ => format!("{:?}", reason),
199 }
200}
201
202pub 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
212pub 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#[derive(Parser, Debug, Clone, Default)]
240#[command(next_help_heading = "Output Options")]
241pub struct OutputArgs {
242 #[arg(long)]
244 pub json: bool,
245}
246
247#[derive(Debug, Default, Serialize)]
249pub struct ExecutionSummary {
250 pub success: bool,
252 pub gas_used: u64,
254 #[serde(skip_serializing_if = "Option::is_none")]
256 pub output: Option<String>,
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub contract_address: Option<Address>,
260 pub logs_count: usize,
262 #[serde(skip_serializing_if = "Option::is_none")]
264 pub revert_reason: Option<String>,
265 #[serde(skip_serializing_if = "Option::is_none")]
267 pub halt_reason: Option<String>,
268 #[serde(skip_serializing_if = "Option::is_none")]
270 pub trace: Option<serde_json::Value>,
271 #[serde(skip_serializing_if = "Option::is_none")]
273 pub state: Option<serde_json::Value>,
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub receipt: Option<serde_json::Value>,
277}
278
279impl ExecutionSummary {
280 pub fn fill_trace_and_dump(
285 &mut self,
286 outcome: &EvmeOutcome,
287 trace_args: &TraceArgs,
288 dump_args: &StateDumpArgs,
289 ) -> Result<(), EvmeError> {
290 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 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 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 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}