1use alloy::primitives::hex::encode;
13use anstyle::{AnsiColor, Color, Style};
14use anvil_zksync_types::traces::{
15 CallLog, CallTrace, CallTraceArena, CallTraceNode, DecodedCallData, ExecutionResultDisplay,
16 L2L1Log, L2L1Logs, TraceMemberOrder,
17};
18use colorchoice::ColorChoice;
19use std::io::{self, Write};
20use std::str;
21use zksync_multivm::interface::CallType;
22use zksync_types::zk_evm_types::FarCallOpcode;
23
24use crate::format::PrettyDecodedValue;
25
26const PIPE: &str = " │ ";
27const EDGE: &str = " └─ ";
28const BRANCH: &str = " ├─ ";
29const CALL: &str = "→ ";
30const RETURN: &str = "← ";
31
32const TRACE_KIND_STYLE: Style = AnsiColor::Yellow.on_default();
33const LOG_STYLE: Style = AnsiColor::Cyan.on_default();
34
35#[derive(Clone, Debug)]
37#[allow(missing_copy_implementations)]
38pub struct TraceWriterConfig {
39 use_colors: bool,
40 write_bytecodes: bool,
41}
42
43impl Default for TraceWriterConfig {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl TraceWriterConfig {
50 pub fn new() -> Self {
52 Self {
53 use_colors: use_colors(ColorChoice::Auto),
54 write_bytecodes: false,
55 }
56 }
57
58 pub fn color_choice(mut self, choice: ColorChoice) -> Self {
60 self.use_colors = use_colors(choice);
61 self
62 }
63
64 pub fn get_use_colors(&self) -> bool {
66 self.use_colors
67 }
68
69 pub fn write_bytecodes(mut self, yes: bool) -> Self {
72 self.write_bytecodes = yes;
73 self
74 }
75
76 pub fn get_write_bytecodes(&self) -> bool {
78 self.write_bytecodes
79 }
80}
81
82#[derive(Clone, Debug)]
86pub struct TraceWriter<W> {
87 writer: W,
88 indentation_level: u16,
89 config: TraceWriterConfig,
90}
91
92impl<W: Write> TraceWriter<W> {
93 #[inline]
95 pub fn new(writer: W) -> Self {
96 Self::with_config(writer, TraceWriterConfig::new())
97 }
98
99 pub fn with_config(writer: W, config: TraceWriterConfig) -> Self {
101 Self {
102 writer,
103 indentation_level: 0,
104 config,
105 }
106 }
107
108 #[inline]
110 pub fn use_colors(mut self, color_choice: ColorChoice) -> Self {
111 self.config.use_colors = use_colors(color_choice);
112 self
113 }
114
115 #[inline]
117 pub fn with_indentation_level(mut self, level: u16) -> Self {
118 self.indentation_level = level;
119 self
120 }
121
122 #[inline]
124 pub fn write_bytecodes(mut self, yes: bool) -> Self {
125 self.config.write_bytecodes = yes;
126 self
127 }
128
129 #[inline]
131 pub const fn writer(&self) -> &W {
132 &self.writer
133 }
134
135 #[inline]
137 pub fn writer_mut(&mut self) -> &mut W {
138 &mut self.writer
139 }
140
141 #[inline]
143 pub fn into_writer(self) -> W {
144 self.writer
145 }
146
147 pub fn write_arena(&mut self, arena: &CallTraceArena) -> io::Result<()> {
149 let root_node = &arena.arena[0];
150 for &child_idx in &root_node.children {
151 self.write_node(arena.nodes(), child_idx)?;
152 }
153 self.writer.flush()
154 }
155
156 fn write_item(
167 &mut self,
168 nodes: &[CallTraceNode],
169 node_idx: usize,
170 item_idx: usize,
171 ) -> io::Result<usize> {
172 let node = &nodes[node_idx];
173 match &node.ordering[item_idx] {
174 TraceMemberOrder::Log(index) => {
175 self.write_log(&node.logs[*index])?;
176 Ok(item_idx + 1)
177 }
178 TraceMemberOrder::Call(index) => {
179 assert!(*index < node.children.len(), "Call index out of bounds");
180 self.write_node(nodes, node.children[*index])?;
181 Ok(item_idx + 1)
182 }
183 TraceMemberOrder::L1L2Log(index) => {
184 self.write_l1_l2_log(&node.l2_l1_logs[*index])?;
185 Ok(item_idx + 1)
186 }
187 }
188 }
189
190 fn write_items_until(
195 &mut self,
196 nodes: &[CallTraceNode],
197 node_idx: usize,
198 first_item_idx: usize,
199 f: impl Fn(usize) -> bool,
200 ) -> io::Result<usize> {
201 let mut item_idx = first_item_idx;
202 while !f(item_idx) {
203 item_idx = self.write_item(nodes, node_idx, item_idx)?;
204 }
205 Ok(item_idx)
206 }
207
208 fn write_items(&mut self, nodes: &[CallTraceNode], node_idx: usize) -> io::Result<()> {
210 let items_cnt = nodes[node_idx].ordering.len();
211 self.write_items_until(nodes, node_idx, 0, |idx| idx == items_cnt)?;
212 Ok(())
213 }
214
215 fn write_node(&mut self, nodes: &[CallTraceNode], idx: usize) -> io::Result<()> {
217 let node = &nodes[idx];
218
219 self.write_branch()?;
221 self.write_trace_header(&node.trace)?;
222 self.writer.write_all(b"\n")?;
223
224 self.indentation_level += 1;
226 self.write_items(nodes, idx)?;
227
228 self.write_edge()?;
230 self.write_trace_footer(&node.trace)?;
231 self.writer.write_all(b"\n")?;
232
233 self.indentation_level -= 1;
234
235 Ok(())
236 }
237
238 fn write_trace_header(&mut self, trace: &CallTrace) -> io::Result<()> {
240 write!(self.writer, "[{}] ", trace.call.gas_used)?;
241
242 let trace_kind_style = self.trace_kind_style();
243 let address = format!("0x{}", encode(trace.call.to));
244
245 match trace.call.r#type {
246 CallType::Create => {
247 write!(
248 self.writer,
249 "{trace_kind_style}{CALL}new{trace_kind_style:#} {label}@{address}",
250 label = trace.decoded.label.as_deref().unwrap_or("<unknown>")
251 )?;
252 if self.config.write_bytecodes {
253 write!(self.writer, "({})", encode(&trace.call.input))?;
254 }
255 }
256 CallType::Call(_) | CallType::NearCall => {
257 let (func_name, inputs) = match &trace.decoded.call_data {
258 Some(DecodedCallData { signature, args }) => {
259 let name = signature.split('(').next().unwrap();
260 (
261 name.to_string(),
262 args.iter()
263 .map(|v| PrettyDecodedValue(v).to_string())
264 .collect::<Vec<_>>()
265 .join(", "),
266 )
267 }
268 None => {
269 if trace.call.input.len() < 4 {
270 ("fallback".to_string(), encode(&trace.call.input))
271 } else {
272 let (selector, data) = trace.call.input.split_at(4);
273 (encode(selector), encode(data))
274 }
275 }
276 };
277
278 write!(
279 self.writer,
280 "{style}{addr}{style:#}::{style}{func_name}{style:#}",
281 style = self.trace_style(trace),
282 addr = trace.decoded.label.as_deref().unwrap_or(&address),
283 )?;
284
285 if !trace.call.value.is_zero() {
286 write!(self.writer, "{{value: {}}}", trace.call.value)?;
287 }
288
289 write!(self.writer, "({inputs})")?;
290
291 let action = match trace.call.r#type {
292 CallType::Call(opcode) => match opcode {
293 FarCallOpcode::Normal => None,
294 FarCallOpcode::Delegate => Some(" [delegatecall]"),
295 FarCallOpcode::Mimic => Some(" [mimiccall]"),
296 },
297 CallType::NearCall => Some("[nearcall]"),
298 CallType::Create => unreachable!(), };
300
301 if let Some(action) = action {
302 write!(
303 self.writer,
304 "{trace_kind_style}{action}{trace_kind_style:#}"
305 )?;
306 }
307 }
308 }
309
310 Ok(())
311 }
312
313 fn write_l1_l2_log(&mut self, log: &L2L1Logs) -> io::Result<()> {
314 let log_style = self.log_style();
315 self.write_branch()?; let (log_type, l2_log) = match &log.raw_log {
318 L2L1Log::User(user_log) => ("UserL2L1Log", &user_log.0),
319 L2L1Log::System(system_log) => ("SystemL2L1Log", &system_log.0),
320 };
321
322 write!(
323 self.writer,
324 "{log_type}({log_style}key: {:?}, value: {:?}{log_style:#})",
325 l2_log.key, l2_log.value
326 )?;
327
328 writeln!(self.writer)?;
329
330 Ok(())
331 }
332
333 fn write_log(&mut self, log: &CallLog) -> io::Result<()> {
334 let log_style = self.log_style();
335 self.write_branch()?;
336
337 if let Some(name) = &log.decoded.name {
338 write!(self.writer, "emit {name}({log_style}")?;
339 if let Some(params) = &log.decoded.params {
340 for (i, (param_name, value)) in params.iter().enumerate() {
341 if i > 0 {
342 self.writer.write_all(b", ")?;
343 }
344 write!(self.writer, "{param_name}: {value}")?;
345 }
346 }
347 writeln!(self.writer, "{log_style:#})")?;
348 } else {
349 for (i, topic) in log.raw_log.indexed_topics.iter().enumerate() {
350 if i == 0 {
351 self.writer.write_all(b" emit topic 0")?;
352 } else {
353 self.write_pipes()?;
354 write!(self.writer, " topic {i}")?;
355 }
356 writeln!(self.writer, ": {log_style}{topic}{log_style:#}")?;
357 }
358 if !log.raw_log.indexed_topics.is_empty() {
359 self.write_pipes()?;
360 }
361 writeln!(
362 self.writer,
363 " data: {log_style}{data}{log_style:#}",
364 data = encode(&log.raw_log.value)
365 )?;
366 }
367 Ok(())
368 }
369
370 fn write_trace_footer(&mut self, trace: &CallTrace) -> io::Result<()> {
372 let status_str = trace.execution_result.display();
374
375 write!(
377 self.writer,
378 "{style}{RETURN}[{status}]{style:#}",
379 style = self.trace_style(trace),
380 status = status_str,
381 )?;
382
383 let decoded_return_data = &trace.decoded.return_data.to_string();
385 if !decoded_return_data.is_empty() {
386 write!(self.writer, " ")?;
387 return self.writer.write_all(decoded_return_data.as_bytes());
388 }
389
390 if !self.config.write_bytecodes
392 && matches!(trace.call.r#type, CallType::Create)
393 && !trace.execution_result.is_failed()
394 {
395 write!(self.writer, " {} bytes of code", trace.call.output.len())?;
396 } else if !trace.call.output.is_empty() {
397 write!(self.writer, " {}", encode(&trace.call.output))?;
398 }
399
400 Ok(())
401 }
402
403 fn write_indentation(&mut self) -> io::Result<()> {
404 self.writer.write_all(b" ")?;
405 for _ in 1..self.indentation_level {
406 self.writer.write_all(PIPE.as_bytes())?;
407 }
408 Ok(())
409 }
410
411 #[doc(alias = "left_prefix")]
412 fn write_branch(&mut self) -> io::Result<()> {
413 self.write_indentation()?;
414 if self.indentation_level != 0 {
415 self.writer.write_all(BRANCH.as_bytes())?;
416 }
417 Ok(())
418 }
419
420 #[doc(alias = "right_prefix")]
421 fn write_pipes(&mut self) -> io::Result<()> {
422 self.write_indentation()?;
423 self.writer.write_all(PIPE.as_bytes())
424 }
425
426 fn write_edge(&mut self) -> io::Result<()> {
427 self.write_indentation()?;
428 self.writer.write_all(EDGE.as_bytes())
429 }
430
431 fn trace_style(&self, trace: &CallTrace) -> Style {
432 if !self.config.use_colors {
433 return Style::default();
434 }
435 let color = if trace.success {
436 AnsiColor::Green
437 } else {
438 AnsiColor::Red
439 };
440 Color::Ansi(color).on_default()
441 }
442
443 fn trace_kind_style(&self) -> Style {
444 if !self.config.use_colors {
445 return Style::default();
446 }
447 TRACE_KIND_STYLE
448 }
449
450 fn log_style(&self) -> Style {
451 if !self.config.use_colors {
452 return Style::default();
453 }
454 LOG_STYLE
455 }
456}
457
458fn use_colors(choice: ColorChoice) -> bool {
459 use io::IsTerminal;
460 match choice {
461 ColorChoice::Auto => io::stdout().is_terminal(),
462 ColorChoice::AlwaysAnsi | ColorChoice::Always => true,
463 ColorChoice::Never => false,
464 }
465}