use std::fmt::Write;
use revm_inspectors::tracing::{types::CallTraceNode, CallTraceArena};
pub struct TraceRenderer {
include_gas: bool,
include_return_data: bool,
max_depth: Option<usize>,
}
impl TraceRenderer {
pub fn new() -> Self {
Self { include_gas: true, include_return_data: true, max_depth: None }
}
pub fn render_arena(&self, arena: &CallTraceArena) -> String {
let mut output = String::new();
if !arena.nodes().is_empty() {
self.render_node(&mut output, arena, 0, 0);
}
output
}
fn render_node(
&self,
output: &mut String,
arena: &CallTraceArena,
node_index: usize,
depth: usize,
) {
if let Some(max_depth) = self.max_depth {
if depth >= max_depth {
return;
}
}
let nodes = arena.nodes();
if node_index >= nodes.len() {
return;
}
let node = &nodes[node_index];
let indent = " ".repeat(depth);
let _ = write!(output, "{}", indent);
self.render_trace(output, node, depth == 0);
for &child_index in &node.children {
self.render_node(output, arena, child_index, depth + 1);
}
}
fn render_trace(&self, output: &mut String, node: &CallTraceNode, _is_root: bool) {
let trace = &node.trace;
let decoded = trace
.decoded
.as_ref()
.map(|d| d.as_ref());
let address_str = if let Some(decoded) = decoded {
if let Some(label) = &decoded.label {
format!("{}[{}]", label, trace.address)
} else {
format!("{}", trace.address)
}
} else {
format!("{}", trace.address)
};
let call_str = if let Some(decoded) = decoded {
if let Some(call_data) = &decoded.call_data {
if call_data.args.is_empty() {
call_data.signature.clone()
} else {
format!(
"{}({})",
call_data
.signature
.split('(')
.next()
.unwrap_or(&call_data.signature),
call_data.args.join(", ")
)
}
} else if trace.data.is_empty() {
"()".to_string()
} else {
format!("0x{}", hex::encode(&trace.data))
}
} else if trace.data.is_empty() {
"()".to_string()
} else {
format!("0x{}", hex::encode(&trace.data))
};
let _ = write!(output, "[{}] {}::{}", trace.depth, address_str, call_str);
if self.include_gas {
let _ = write!(output, " [gas: {}]", trace.gas_used);
}
if trace.success {
let _ = write!(output, " ✓");
} else {
let _ = write!(output, " ✗");
}
let _ = writeln!(output);
if self.include_return_data {
if let Some(decoded) = decoded {
if let Some(return_data) = &decoded.return_data {
let return_indent = " ".repeat(trace.depth + 1);
let _ = writeln!(output, "{}→ {}", return_indent, return_data);
}
} else if !trace.output.is_empty() {
let return_indent = " ".repeat(trace.depth + 1);
let _ = writeln!(output, "{}→ 0x{}", return_indent, hex::encode(&trace.output));
}
}
for log in &node.logs {
self.render_log(output, log);
}
}
fn render_log(&self, output: &mut String, log: &revm_inspectors::tracing::types::CallLog) {
let indent = " "; let _ = write!(output, "{}├─ ", indent);
if let Some(decoded) = &log.decoded {
if let Some(name) = &decoded.name {
let _ = write!(output, "emit {}(", name);
if let Some(params) = &decoded.params {
let param_strs: Vec<String> = params
.iter()
.map(|(name, value)| format!("{}: {}", name, value))
.collect();
let _ = write!(output, "{}", param_strs.join(", "));
}
let _ = write!(output, ")");
} else {
let _ = write!(output, "emit <unknown>");
}
} else {
if !log.raw_log.topics().is_empty() {
let _ =
write!(output, "emit 0x{}", hex::encode(log.raw_log.topics()[0].as_slice()));
if log.raw_log.topics().len() > 1 {
let _ = write!(output, " (+ {} topics)", log.raw_log.topics().len() - 1);
}
} else {
let _ = write!(output, "emit <no topics>");
}
}
let _ = writeln!(output);
}
}
impl Default for TraceRenderer {
fn default() -> Self {
Self::new()
}
}
pub fn render_trace_arena(arena: &CallTraceArena) -> String {
TraceRenderer::new().render_arena(arena)
}