1use super::{
4 context::{
5 ActiveInternalCallCache, ActiveInternalCallLocation, Prompt, StatusKind, TUIContext,
6 write_pretty_opcode,
7 },
8 storage::{StorageAccess, StorageSpace, hex_u256, storage_access_at, storage_values},
9};
10use crate::{DebuggerLayout, debugger::DebuggerStats, op::OpcodeParam};
11use alloy_dyn_abi::{DynSolType, Specifier, parser::Parameters};
12use alloy_primitives::{Address, U256, keccak256};
13use foundry_common::fmt::format_token;
14use foundry_evm_core::buffer::{BufferKind, get_buffer_accesses};
15use foundry_evm_traces::debug::{
16 DebugSourceScope, DebugVariable, decode_step_parameters, function_signature,
17};
18use ratatui::{
19 Frame,
20 layout::{Alignment, Constraint, Direction, Layout, Rect},
21 style::{Color, Modifier, Style},
22 text::{Line, Span, Text},
23 widgets::{Block, Borders, List, ListItem, ListState, Paragraph, Wrap},
24};
25use revm::interpreter::InstructionResult;
26use revm_inspectors::tracing::types::{CallKind, DecodedInternalCall, DecodedTraceStep};
27use std::{collections::VecDeque, fmt::Write};
28
29impl TUIContext<'_> {
30 pub(crate) fn draw_layout(&mut self, f: &mut Frame<'_>) {
31 let area = f.area();
33 let min_width = if self.show_data || self.show_stack { 100 } else { 1 };
34 let min_height = 16;
35 if area.width < min_width || area.height < min_height {
36 self.size_too_small(f, min_width, min_height);
37 return;
38 }
39
40 match self.layout() {
41 DebuggerLayout::Horizontal => self.horizontal_layout(f),
42 DebuggerLayout::Vertical => self.vertical_layout(f),
43 DebuggerLayout::Auto => {
44 let min_column_width_for_horizontal = 200;
46 if area.width >= min_column_width_for_horizontal {
47 self.horizontal_layout(f);
48 } else {
49 self.vertical_layout(f);
50 }
51 }
52 }
53 }
54
55 fn size_too_small(&self, f: &mut Frame<'_>, min_width: u16, min_height: u16) {
56 let mut lines = Vec::with_capacity(4);
57
58 let l1 = "Terminal size too small:";
59 lines.push(Line::from(l1));
60
61 let area = f.area();
62 let width_color = if area.width >= min_width { Color::Green } else { Color::Red };
63 let height_color = if area.height >= min_height { Color::Green } else { Color::Red };
64 let l2 = vec![
65 Span::raw("Width = "),
66 Span::styled(area.width.to_string(), Style::new().fg(width_color)),
67 Span::raw(" Height = "),
68 Span::styled(area.height.to_string(), Style::new().fg(height_color)),
69 ];
70 lines.push(Line::from(l2));
71
72 let l3 = "Needed for current config:";
73 lines.push(Line::from(l3));
74 let l4 = format!("Width = {min_width} Height = {min_height}");
75 lines.push(Line::from(l4));
76
77 let paragraph =
78 Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: true });
79 f.render_widget(paragraph, area)
80 }
81
82 fn vertical_layout(&mut self, f: &mut Frame<'_>) {
100 let area = f.area();
101 let footer_height = self.footer_height();
102
103 let [app, footer] = Layout::new(
105 Direction::Vertical,
106 [Constraint::Min(0), Constraint::Length(footer_height)],
107 )
108 .split(area)[..] else {
109 unreachable!()
110 };
111
112 if footer_height > 0 {
113 self.draw_footer(f, footer);
114 }
115
116 let opcodes_weight = if self.show_opcodes { 1 } else { 0 };
117 let source_weight = if self.show_source { 3 } else { 0 };
118 let data_weight = if self.show_data { if self.show_source { 1 } else { 2 } } else { 0 };
119 let total_weight = opcodes_weight
120 + data_weight
121 + source_weight
122 + if self.show_variables { 1 } else { 0 }
123 + if self.show_stack { 1 } else { 0 };
124 let mut constraints = Vec::with_capacity(5);
125 if self.show_opcodes {
126 constraints.push(Constraint::Ratio(opcodes_weight, total_weight));
127 }
128 if self.show_variables {
129 constraints.push(Constraint::Ratio(1, total_weight));
130 }
131 if self.show_stack {
132 constraints.push(Constraint::Ratio(1, total_weight));
133 }
134 if self.show_data {
135 constraints.push(Constraint::Ratio(data_weight, total_weight));
136 }
137 if self.show_source {
138 constraints.push(Constraint::Ratio(source_weight, total_weight));
139 }
140
141 let panes = Layout::new(Direction::Vertical, constraints).split(app);
142 let mut panes = panes.iter();
143 if self.show_opcodes {
144 self.draw_op_list(f, *panes.next().expect("opcodes pane is visible"));
145 }
146 if self.show_variables {
147 self.draw_variables(f, *panes.next().expect("variables pane is visible"));
148 }
149 if self.show_stack {
150 self.draw_stack(f, *panes.next().expect("stack pane is visible"));
151 }
152 if self.show_data {
153 self.draw_data(f, *panes.next().expect("data pane is visible"));
154 }
155 if self.show_source {
156 self.draw_src(f, *panes.next().expect("source pane is visible"));
157 }
158 }
159
160 fn horizontal_layout(&mut self, f: &mut Frame<'_>) {
174 let area = f.area();
175 let footer_height = self.footer_height();
176
177 let [app, footer] = Layout::new(
179 Direction::Vertical,
180 [Constraint::Min(0), Constraint::Length(footer_height)],
181 )
182 .split(area)[..] else {
183 unreachable!()
184 };
185
186 let has_left_pane = self.show_opcodes || self.show_source;
187 let has_right_pane = self.show_variables || self.show_stack || self.show_data;
188 let (app_left, app_right) = match (has_left_pane, has_right_pane) {
189 (true, true) => {
190 let [app_left, app_right] = Layout::new(
191 Direction::Horizontal,
192 [Constraint::Ratio(1, 2), Constraint::Ratio(1, 2)],
193 )
194 .split(app)[..] else {
195 unreachable!()
196 };
197 (Some(app_left), Some(app_right))
198 }
199 (true, false) => (Some(app), None),
200 (false, true) => (None, Some(app)),
201 (false, false) => (None, None),
202 };
203
204 if footer_height > 0 {
205 self.draw_footer(f, footer);
206 }
207 if let Some(app_left) = app_left {
208 let total_weight =
209 if self.show_opcodes { 1 } else { 0 } + if self.show_source { 3 } else { 0 };
210 let mut constraints = Vec::with_capacity(2);
211 if self.show_opcodes {
212 constraints.push(Constraint::Ratio(1, total_weight));
213 }
214 if self.show_source {
215 constraints.push(Constraint::Ratio(3, total_weight));
216 }
217
218 let panes = Layout::new(Direction::Vertical, constraints).split(app_left);
219 let mut panes = panes.iter();
220 if self.show_opcodes {
221 self.draw_op_list(f, *panes.next().expect("opcodes pane is visible"));
222 }
223 if self.show_source {
224 self.draw_src(f, *panes.next().expect("source pane is visible"));
225 }
226 }
227
228 if let Some(app_right) = app_right {
229 let total_weight = if self.show_variables { 1 } else { 0 }
230 + if self.show_stack { 1 } else { 0 }
231 + if self.show_data { 2 } else { 0 };
232 let mut constraints = Vec::with_capacity(3);
233 if self.show_variables {
234 constraints.push(Constraint::Ratio(1, total_weight));
235 }
236 if self.show_stack {
237 constraints.push(Constraint::Ratio(1, total_weight));
238 }
239 if self.show_data {
240 constraints.push(Constraint::Ratio(2, total_weight));
241 }
242
243 let panes = Layout::new(Direction::Vertical, constraints).split(app_right);
244 let mut panes = panes.iter();
245 if self.show_variables {
246 self.draw_variables(f, *panes.next().expect("variables pane is visible"));
247 }
248 if self.show_stack {
249 self.draw_stack(f, *panes.next().expect("stack pane is visible"));
250 }
251 if self.show_data {
252 self.draw_data(f, *panes.next().expect("data pane is visible"));
253 }
254 }
255 }
256
257 fn footer_height(&self) -> u16 {
258 let status_or_input = if matches!(self.prompt, Some(Prompt::Command(_))) {
259 3
260 } else {
261 u16::from(self.prompt.is_some() || self.status.is_some())
262 };
263 let shortcuts = if self.show_shortcuts { 3 } else { 0 };
264 status_or_input + shortcuts
265 }
266
267 fn draw_footer(&self, f: &mut Frame<'_>, area: Rect) {
268 if let Some(Prompt::Command(input)) = &self.prompt {
269 self.draw_command_prompt(f, area, input);
270 return;
271 }
272
273 let mut lines = Vec::with_capacity(self.footer_height() as usize);
274
275 if let Some(Prompt::Pc(input)) = &self.prompt {
276 lines.push(Line::from(vec![
277 Span::styled(
278 "Goto PC: ",
279 Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
280 ),
281 Span::raw(input.as_str()),
282 Span::styled("█", Style::new().fg(Color::Cyan)),
283 Span::styled(
284 " Enter: jump | Esc: cancel | hex: 0x2a/2a | decimal: d:42",
285 Style::new().add_modifier(Modifier::DIM),
286 ),
287 ]));
288 } else if let Some(Prompt::BufferOffset(input)) = &self.prompt {
289 lines.push(Line::from(vec![
290 Span::styled(
291 format!("Goto {} offset: ", self.active_buffer_name()),
292 Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
293 ),
294 Span::raw(input.as_str()),
295 Span::styled("█", Style::new().fg(Color::Cyan)),
296 Span::styled(
297 " Enter: jump | Esc: cancel | hex: 0x20/20 | decimal: d:32",
298 Style::new().add_modifier(Modifier::DIM),
299 ),
300 ]));
301 } else if let Some(Prompt::OpcodeSearch(input)) = &self.prompt {
302 lines.push(Line::from(vec![
303 Span::styled(
304 "Search opcodes: /",
305 Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD),
306 ),
307 Span::raw(input.as_str()),
308 Span::styled("█", Style::new().fg(Color::Cyan)),
309 Span::styled(
310 " Enter: jump | Esc: cancel | after search: n/N repeat",
311 Style::new().add_modifier(Modifier::DIM),
312 ),
313 ]));
314 } else if let Some(status) = &self.status {
315 let style = match status.kind {
316 StatusKind::Info => Style::new().fg(Color::Green),
317 StatusKind::Error => Style::new().fg(Color::Red).add_modifier(Modifier::BOLD),
318 };
319 lines.push(Line::from(Span::styled(status.text.as_str(), style)));
320 }
321
322 if self.show_shortcuts {
323 lines.extend(shortcut_lines());
324 }
325
326 let paragraph =
327 Paragraph::new(lines).alignment(Alignment::Center).wrap(Wrap { trim: false });
328 f.render_widget(paragraph, area);
329 }
330
331 fn draw_command_prompt(&self, f: &mut Frame<'_>, area: Rect, input: &str) {
332 let shortcuts = if self.show_shortcuts { 3 } else { 0 };
333 let [prompt, shortcuts_area] = Layout::new(
334 Direction::Vertical,
335 [Constraint::Length(3), Constraint::Length(shortcuts)],
336 )
337 .split(area)[..] else {
338 unreachable!()
339 };
340
341 let prompt_line = command_prompt_line(input, prompt.width.saturating_sub(2));
342 let block = Block::default().title("Command").borders(Borders::ALL);
343 let paragraph = Paragraph::new(prompt_line).block(block);
344 f.render_widget(paragraph, prompt);
345
346 if self.show_shortcuts {
347 self.draw_shortcuts(f, shortcuts_area);
348 }
349 }
350
351 fn draw_shortcuts(&self, f: &mut Frame<'_>, area: Rect) {
352 let paragraph = Paragraph::new(shortcut_lines())
353 .alignment(Alignment::Center)
354 .wrap(Wrap { trim: false });
355 f.render_widget(paragraph, area);
356 }
357
358 fn draw_src(&self, f: &mut Frame<'_>, area: Rect) {
359 let (text_output, source_name) = self.src_text(area);
360 let call_kind_text = match self.call_kind() {
361 CallKind::Create | CallKind::Create2 => "Contract creation",
362 CallKind::Call => "Contract call",
363 CallKind::StaticCall => "Contract staticcall",
364 CallKind::CallCode => "Contract callcode",
365 CallKind::DelegateCall => "Contract delegatecall",
366 CallKind::AuthCall => "Contract authcall",
367 };
368 let title = source_pane_title(
369 call_kind_text,
370 source_name,
371 self.current_step_notice_text().map(step_notice_title),
372 );
373 let block = Block::default().title(title).borders(Borders::ALL);
374 let paragraph = Paragraph::new(text_output).block(block).wrap(Wrap { trim: false });
375 f.render_widget(paragraph, area);
376 }
377
378 fn src_text(&self, area: Rect) -> (Text<'_>, Option<&str>) {
379 let (source_element, source) = match self.src_map() {
380 Ok(r) => r,
381 Err(e) => return (Text::from(e), None),
382 };
383
384 let offset = source_element.offset() as usize;
389 let len = source_element.length() as usize;
390 let max = source.source.len();
391
392 let actual_start = offset.min(max);
394 let actual_end = (offset + len).min(max);
395
396 let mut before: Vec<_> = source.source[..actual_start].split_inclusive('\n').collect();
397 let actual: Vec<_> =
398 source.source[actual_start..actual_end].split_inclusive('\n').collect();
399 let mut after: VecDeque<_> = source.source[actual_end..].split_inclusive('\n').collect();
400
401 let num_lines = before.len() + actual.len() + after.len();
402 let height = area.height as usize;
403 let needed_highlight = actual.len();
404 let mid_len = before.len() + actual.len();
405
406 let (start_line, end_line) = if needed_highlight > height {
408 let start_line = before.len().saturating_sub(1);
410 (start_line, before.len() + needed_highlight)
411 } else if height > num_lines {
412 (0, num_lines)
414 } else {
415 let remaining = height - needed_highlight;
416 let mut above = remaining / 2;
417 let mut below = remaining / 2;
418 if below > after.len() {
419 above += below - after.len();
421 } else if above > before.len() {
422 below += above - before.len();
424 } else {
425 }
427
428 if above == 0 {
432 above = 1;
433 }
434 (before.len().saturating_sub(above), mid_len + below)
435 };
436
437 let u_num = Style::new().fg(Color::Gray);
439 let u_text = Style::new().add_modifier(Modifier::DIM);
441 let h_num = Style::new().fg(Color::Cyan);
443 let h_text = Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD);
445
446 let mut lines = SourceLines::new(start_line, end_line);
447
448 if let Some(last) = before.pop() {
450 let last_has_nl = last.ends_with('\n');
451
452 if last_has_nl {
453 before.push(last);
454 }
455 for line in &before[start_line..] {
456 lines.push(u_num, line, u_text);
457 }
458
459 let first = if last_has_nl {
460 0
461 } else {
462 lines.push_raw(h_num, &[Span::raw(last), Span::styled(actual[0], h_text)]);
463 1
464 };
465
466 for line in &actual[first..] {
468 lines.push(h_num, line, h_text);
469 }
470 } else {
471 for line in &actual {
473 lines.push(h_num, line, h_text);
474 }
475 }
476
477 if let Some(last) = actual.last()
479 && !last.ends_with('\n')
480 && let Some(post) = after.pop_front()
481 && let Some(last) = lines.lines.last_mut()
482 {
483 last.spans.push(Span::raw(post));
484 }
485
486 while mid_len + after.len() > end_line {
488 after.pop_back();
489 }
490 for line in after {
491 lines.push(u_num, line, u_text);
492 }
493
494 for line in &mut lines.lines {
496 if area.width as usize > line.width() + 1 {
498 line.push_span(Span::raw(" ".repeat(area.width as usize - line.width() - 1)));
499 }
500 }
501
502 (Text::from(lines.lines), source.path.to_str())
503 }
504
505 fn current_step_notice_text(&self) -> Option<&str> {
506 let DecodedTraceStep::Line(line) = self.current_step().decoded.as_deref()? else {
507 return None;
508 };
509 (!line.is_empty()).then_some(line.as_str())
510 }
511
512 fn draw_op_list(&self, f: &mut Frame<'_>, area: Rect) {
513 let debug_steps = self.debug_steps();
514 let visible_rows = area.height.saturating_sub(2) as usize;
516 let scroll_padding = usize::from(visible_rows >= 3);
517 let end = debug_steps
518 .len()
519 .min(visible_rows.max(self.current_step.saturating_add(scroll_padding + 1)));
520 let start = end.saturating_sub(visible_rows);
521 let pc_width = hex_digits(self.opcode_max_pc());
522 let items = debug_steps[start..end].iter().map(|step| {
523 let mut row = String::with_capacity(pc_width + 1 + 8);
524 write!(row, "{:0>pc_width$x}|", step.pc).unwrap();
525 write_pretty_opcode(&mut row, step);
526 ListItem::new(Span::styled(row, Style::new().fg(Color::White)))
527 });
528
529 let step = self.current_step();
530 let call_gas_used = self.debug_call().gas_limit.saturating_sub(step.gas_remaining);
531 let title = op_list_title(
532 self.address(),
533 step.pc,
534 step.gas_remaining,
535 call_gas_used,
536 step.gas_refund_counter,
537 self.debugger_context.stats,
538 );
539 let block = Block::default().title(title).borders(Borders::ALL);
540 let list = List::new(items)
541 .block(block)
542 .highlight_symbol("▶")
543 .highlight_style(Style::new().fg(Color::White).bg(Color::DarkGray))
544 .scroll_padding(scroll_padding);
545 let mut state =
546 ListState::default().with_selected(Some(self.current_step.saturating_sub(start)));
547 f.render_stateful_widget(list, area, &mut state);
548 }
549
550 fn draw_stack(&self, f: &mut Frame<'_>, area: Rect) {
551 let step = self.current_step();
552 let stack = step.stack.as_ref();
553 let stack_len = stack.map_or(0, |s| s.len());
554
555 let min_len = decimal_digits(stack_len).max(2);
556
557 let params = OpcodeParam::of(step.op.get());
558
559 let text: Vec<Line<'_>> = stack
560 .map(|stack| {
561 stack
562 .iter()
563 .rev()
564 .enumerate()
565 .skip(self.draw_memory.current_stack_startline)
566 .map(|(i, stack_item)| {
567 let param = params.iter().find(|param| param.index == i);
568 stack_item_line(i, min_len, stack_item, param, self.stack_labels)
569 })
570 .collect()
571 })
572 .unwrap_or_default();
573
574 let title = format!("Stack: {stack_len}");
575 let block = Block::default().title(title).borders(Borders::ALL);
576 let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
577 f.render_widget(paragraph, area);
578 }
579
580 fn draw_variables(&mut self, f: &mut Frame<'_>, area: Rect) {
581 let variables = self.scope_variables();
582 let storage_access = self.current_storage_access_line();
583 let step_notice = self.current_step_notice_text().map(step_notice_line);
584 let known = variables.iter().filter(|variable| variable.value.is_some()).count();
585 let title = variables_title(
586 variables.len(),
587 known,
588 storage_access.is_some(),
589 step_notice.is_some(),
590 );
591
592 let mut text = variables.into_iter().map(scope_variable_line).collect::<Vec<_>>();
593 if let Some(step_notice) = step_notice {
594 if !text.is_empty() {
595 text.push(Line::from(""));
596 }
597 text.push(step_notice);
598 }
599 if let Some(storage_access) = storage_access {
600 if !text.is_empty() {
601 text.push(Line::from(""));
602 }
603 text.push(storage_access);
604 }
605
606 if text.is_empty() {
607 text.push(Line::from(Span::styled(
608 "No variables in current scope",
609 Style::new().add_modifier(Modifier::DIM),
610 )));
611 }
612
613 let block = Block::default().title(title).borders(Borders::ALL);
614 let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
615 f.render_widget(paragraph, area);
616 }
617
618 fn current_storage_access_line(&self) -> Option<Line<'static>> {
619 storage_access_at(self.debug_steps(), self.current_step).map(|access| {
620 let values = (access.space() == StorageSpace::Persistent)
621 .then(|| storage_values(&self.storage_accesses(access.space())));
622 let next_values = (access.space() == StorageSpace::Persistent)
623 .then(|| self.next_storage_write_values());
624 let label = self.storage_label(
625 access.space(),
626 access.slot(),
627 values.as_ref(),
628 next_values.as_ref(),
629 );
630 storage_access_line(access, label.as_deref())
631 })
632 }
633
634 fn draw_data(&mut self, f: &mut Frame<'_>, area: Rect) {
635 if let Some(space) = self.active_storage() {
636 self.draw_storage(f, area, space);
637 } else {
638 self.draw_buffer(f, area);
639 }
640 }
641
642 fn draw_storage(&mut self, f: &mut Frame<'_>, area: Rect, space: StorageSpace) {
643 let accesses = self.storage_accesses(space);
644 let values = (space == StorageSpace::Persistent).then(|| storage_values(&accesses));
645 let next_values =
646 (space == StorageSpace::Persistent).then(|| self.next_storage_write_values());
647 let current_slot = storage_access_at(self.debug_steps(), self.current_step)
648 .filter(|access| access.space() == space)
649 .map(StorageAccess::slot);
650 self.draw_memory.current_storage_startline =
651 self.draw_memory.current_storage_startline.min(accesses.len().saturating_sub(1));
652
653 let index_width = decimal_digits(accesses.len()).max(2);
654 let mut lines = accesses
655 .values()
656 .copied()
657 .enumerate()
658 .skip(self.draw_memory.current_storage_startline)
659 .flat_map(|(index, access)| {
660 let label =
661 self.storage_label(space, access.slot(), values.as_ref(), next_values.as_ref());
662 storage_slot_lines(
663 index,
664 index_width,
665 access,
666 label.as_deref(),
667 current_slot == Some(access.slot()),
668 )
669 })
670 .collect::<Vec<_>>();
671 if lines.is_empty() {
672 lines.push(Line::from(Span::styled(
673 format!("No {} accessed in current call", space.noun()),
674 Style::new().add_modifier(Modifier::DIM),
675 )));
676 }
677
678 let count = accesses.len();
679 let suffix = if count == 1 { "slot" } else { "slots" };
680 let title = format!("{}: {count} accessed {suffix}", space.label());
681 let block = Block::default().title(title).borders(Borders::ALL);
682 let paragraph = Paragraph::new(lines).block(block).wrap(Wrap { trim: false });
683 f.render_widget(paragraph, area);
684 }
685
686 fn draw_buffer(&self, f: &mut Frame<'_>, area: Rect) {
687 let call = self.debug_call();
688 let step = self.current_step();
689 let buf: &[u8] = match self.active_buffer {
690 BufferKind::Memory => step.memory.as_ref().map_or(&[], |memory| memory.as_ref()),
691 BufferKind::Calldata => call.calldata.as_ref(),
692 BufferKind::Returndata => step.returndata.as_ref(),
693 };
694
695 let min_len = hex_digits(buf.len());
696
697 let mut offset = None;
699 let mut len = None;
700 let mut write_offset = None;
701 let mut write_size = None;
702 let mut color = None;
703 let stack_len = step.stack.as_ref().map_or(0, |s| s.len());
704 if stack_len > 0
705 && let Some(stack) = step.stack.as_ref()
706 && let Some(accesses) = get_buffer_accesses(step.op.get(), stack)
707 {
708 if let Some(read_access) = accesses.read {
709 offset = Some(read_access.1.offset);
710 len = Some(read_access.1.len);
711 color = Some(Color::Cyan);
712 }
713 if let Some(write_access) = accesses.write
714 && self.active_buffer == BufferKind::Memory
715 {
716 write_offset = Some(write_access.offset);
717 write_size = Some(write_access.len);
718 }
719 }
720
721 if self.current_step > 0 {
726 let prev_step = self.current_step - 1;
727 let prev_step = &self.debug_steps()[prev_step];
728 if let Some(stack) = prev_step.stack.as_ref()
729 && let Some(write_access) =
730 get_buffer_accesses(prev_step.op.get(), stack).and_then(|a| a.write)
731 && self.active_buffer == BufferKind::Memory
732 {
733 offset = Some(write_access.offset);
734 len = Some(write_access.len);
735 color = Some(Color::Green);
736 }
737 }
738
739 let height = area.height as usize;
740 let end_line = self.draw_memory.current_buf_startline + height;
741
742 let text: Vec<Line<'_>> = buf
743 .chunks(32)
744 .enumerate()
745 .skip(self.draw_memory.current_buf_startline)
746 .take_while(|(i, _)| *i < end_line)
747 .map(|(i, buf_word)| {
748 let mut spans = Vec::with_capacity(1 + 32 * 2 + 1 + 32 / 4 + 1);
749
750 spans.push(Span::styled(
752 format!("{:0min_len$x}| ", i * 32),
753 Style::new().fg(Color::White),
754 ));
755
756 hex_bytes_spans(buf_word, &mut spans, |j, _| {
758 let mut byte_color = Color::White;
759 let mut end = None;
760 let idx = i * 32 + j;
761 if let (Some(offset), Some(len), Some(color)) = (offset, len, color) {
762 let offset_end = offset.saturating_add(len);
763 end = Some(offset_end);
764 if (offset..offset_end).contains(&idx) {
765 byte_color = color;
769 }
770 }
771 if let (Some(write_offset), Some(write_size)) = (write_offset, write_size) {
772 let write_end = write_offset.saturating_add(write_size);
774 if let Some(read_end) = end {
775 let read_start = offset.unwrap();
776 if (write_offset..write_end).contains(&read_end) {
777 if (write_offset..read_end).contains(&idx) {
779 return Style::new().fg(Color::Yellow);
780 }
781 } else if (write_offset..write_end).contains(&read_start) {
782 if (read_start..write_end).contains(&idx) {
785 return Style::new().fg(Color::Yellow);
786 }
787 }
788 }
789 if (write_offset..write_end).contains(&idx) {
790 byte_color = Color::Red;
791 }
792 }
793
794 Style::new().fg(byte_color)
795 });
796
797 if self.buf_utf {
798 spans.push(Span::raw("|"));
799 for utf in buf_word.chunks(4) {
800 if let Ok(utf_str) = std::str::from_utf8(utf) {
801 spans.push(Span::raw(utf_str.replace('\0', ".")));
802 } else {
803 spans.push(Span::raw("."));
804 }
805 }
806 }
807
808 spans.push(Span::raw("\n"));
809
810 Line::from(spans)
811 })
812 .collect();
813
814 let title = self.active_buffer.title(buf.len());
815 let block = Block::default().title(title).borders(Borders::ALL);
816 let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
817 f.render_widget(paragraph, area);
818 }
819}
820
821#[derive(Clone, Debug, PartialEq, Eq)]
822struct ScopeVariable {
823 kind: ScopeVariableKind,
824 name: String,
825 value: Option<String>,
826}
827
828#[derive(Clone, Copy, Debug, PartialEq, Eq)]
829enum ScopeVariableKind {
830 Parameter,
831 Return,
832 Local,
833}
834
835struct ActiveInternalCall<'a> {
836 trace_node_idx: usize,
837 entry_step: usize,
838 end_step: usize,
839 decoded: &'a DecodedInternalCall,
840}
841
842const PRECOMPILE_NOTICE_PREFIX: &str = "precompile:";
843
844impl ScopeVariableKind {
845 const fn label(self) -> &'static str {
846 match self {
847 Self::Parameter => "param",
848 Self::Return => "return",
849 Self::Local => "local",
850 }
851 }
852
853 const fn color(self) -> Color {
854 match self {
855 Self::Parameter => Color::Cyan,
856 Self::Return => Color::Green,
857 Self::Local => Color::White,
858 }
859 }
860}
861
862impl TUIContext<'_> {
863 fn scope_variables(&mut self) -> Vec<ScopeVariable> {
864 let (scope, start) = {
865 let Ok((source_element, source)) = self.src_map() else {
866 return Vec::new();
867 };
868 let start = source_element.offset() as usize;
869 let end = start.saturating_add(source_element.length() as usize);
870 let Some(scope) = source.find_debug_scope(start, end) else {
871 return Vec::new();
872 };
873 (scope.clone(), start)
874 };
875
876 let parameter_values = self.decode_parameter_values(&scope);
877 let return_values = self.decode_return_values(&scope);
878 let mut variables = Vec::new();
879
880 variables.extend(scope.parameters.iter().enumerate().map(|(i, variable)| ScopeVariable {
881 kind: ScopeVariableKind::Parameter,
882 name: variable_name(variable, i, "arg"),
883 value: parameter_values.as_ref().and_then(|values| values.get(i).cloned()),
884 }));
885
886 variables.extend(scope.returns.iter().enumerate().map(|(i, variable)| ScopeVariable {
887 kind: ScopeVariableKind::Return,
888 name: variable_name(variable, i, "ret"),
889 value: return_values.as_ref().and_then(|values| values.get(i).cloned()),
890 }));
891
892 variables.extend(scope.visible_locals(start).enumerate().map(|(i, variable)| {
893 ScopeVariable {
894 kind: ScopeVariableKind::Local,
895 name: variable_name(variable, i, "local"),
896 value: None,
897 }
898 }));
899
900 variables
901 }
902
903 fn decode_parameter_values(&mut self, scope: &DebugSourceScope) -> Option<Vec<String>> {
904 let scope_signature = scope_function_signature(scope);
905 self.decode_internal_parameter_values(scope)
906 .or_else(|| decode_external_parameter_values(scope, &self.debug_call().calldata))
907 .or_else(|| {
908 self.debug_call().decoded.as_ref().and_then(|decoded| {
909 let call_data = decoded.call_data.as_ref()?;
910 scope_signature
911 .as_deref()
912 .is_some_and(|signature| signature == call_data.signature)
913 .then(|| call_data.args.clone())
914 })
915 })
916 }
917
918 fn decode_return_values(&mut self, scope: &DebugSourceScope) -> Option<Vec<String>> {
919 let current_step = self.absolute_current_step();
920 if let Some(values) = self
921 .active_internal_call()
922 .and_then(|active| {
923 (current_step >= active.end_step
924 && decoded_internal_name_matches(&active.decoded.func_name, scope))
925 .then(|| active.decoded.return_data.clone())
926 })
927 .flatten()
928 {
929 return Some(values);
930 }
931
932 if self.current_step + 1 < self.debug_steps().len()
933 || !matches!(
934 self.current_step().status,
935 Some(InstructionResult::Return | InstructionResult::Stop)
936 )
937 {
938 return None;
939 }
940
941 decode_external_return_values(
942 scope,
943 &self.debug_call().calldata,
944 &self.debug_call().returndata,
945 )
946 }
947
948 fn decode_internal_parameter_values(
949 &mut self,
950 scope: &DebugSourceScope,
951 ) -> Option<Vec<String>> {
952 let (args, trace_node_idx, entry_step) = {
953 let active = self.active_internal_call()?;
954 if !decoded_internal_name_matches(&active.decoded.func_name, scope) {
955 return None;
956 }
957
958 (active.decoded.args.clone(), active.trace_node_idx, active.entry_step)
959 };
960
961 if let Some(args) = args {
962 return Some(args);
963 }
964
965 let parameters = Parameters::parse(&scope.parameters_src).ok()?;
966 let node = self.debug_arena().iter().find(|node| {
967 node.trace_node_idx == trace_node_idx
968 && entry_step >= node.step_offset
969 && entry_step < node.step_offset.saturating_add(node.steps.len())
970 })?;
971 let step = node.steps.get(entry_step.checked_sub(node.step_offset)?)?;
972 decode_step_parameters(¶meters, step, Some(node.calldata.as_ref()))
973 }
974
975 fn active_internal_call(&mut self) -> Option<ActiveInternalCall<'_>> {
976 let current_node_idx = self.draw_memory.inner_call_index;
977 let trace_node_idx = self.debug_call().trace_node_idx;
978 let current_step = self.absolute_current_step();
979 let location = if let Some(cache) = self.draw_memory.active_internal_call
980 && cache.matches(current_node_idx, trace_node_idx, current_step)
981 {
982 cache.location
983 } else {
984 let location =
985 self.find_active_internal_call(current_node_idx, trace_node_idx, current_step);
986 self.draw_memory.active_internal_call = Some(ActiveInternalCallCache {
987 current_node_idx,
988 trace_node_idx,
989 absolute_step: current_step,
990 location,
991 });
992 location
993 }?;
994
995 self.active_internal_call_at(location)
996 }
997
998 fn find_active_internal_call(
999 &self,
1000 current_node_idx: usize,
1001 trace_node_idx: usize,
1002 current_step: usize,
1003 ) -> Option<ActiveInternalCallLocation> {
1004 let mut active = None;
1005
1006 for (node_idx, node) in
1007 self.debug_arena().iter().enumerate().take(current_node_idx.saturating_add(1))
1008 {
1009 if node.trace_node_idx != trace_node_idx {
1010 continue;
1011 }
1012
1013 for (step_idx, step) in node.steps.iter().enumerate() {
1014 let marker_step = node.step_offset.saturating_add(step_idx);
1015 if marker_step > current_step {
1016 break;
1017 }
1018
1019 let Some(decoded) = step.decoded.as_deref() else { continue };
1020 let DecodedTraceStep::InternalCall(_, end_step) = decoded else { continue };
1021 if current_step <= *end_step {
1022 active = Some(ActiveInternalCallLocation {
1023 trace_node_idx,
1024 marker_node_idx: node_idx,
1025 marker_step_idx: step_idx,
1026 entry_step: marker_step.saturating_add(1),
1027 end_step: *end_step,
1028 });
1029 }
1030 }
1031 }
1032
1033 active
1034 }
1035
1036 fn active_internal_call_at(
1037 &self,
1038 location: ActiveInternalCallLocation,
1039 ) -> Option<ActiveInternalCall<'_>> {
1040 let step = self
1041 .debug_arena()
1042 .get(location.marker_node_idx)?
1043 .steps
1044 .get(location.marker_step_idx)?;
1045 let DecodedTraceStep::InternalCall(decoded, end_step) = step.decoded.as_deref()? else {
1046 return None;
1047 };
1048 (*end_step == location.end_step).then_some(ActiveInternalCall {
1049 trace_node_idx: location.trace_node_idx,
1050 entry_step: location.entry_step,
1051 end_step: location.end_step,
1052 decoded,
1053 })
1054 }
1055
1056 fn absolute_current_step(&self) -> usize {
1057 self.debug_call().step_offset.saturating_add(self.current_step)
1058 }
1059}
1060
1061fn source_pane_title(
1062 call_kind_text: &str,
1063 source_name: Option<&str>,
1064 step_notice: Option<&str>,
1065) -> String {
1066 let mut title = call_kind_text.to_string();
1067 if let Some(step_notice) = step_notice {
1068 write!(title, " | {step_notice}").unwrap();
1069 }
1070 if let Some(source_name) = source_name {
1071 write!(title, " | {source_name}").unwrap();
1072 }
1073 title.push(' ');
1074 title
1075}
1076
1077fn variables_title(
1078 total_variables: usize,
1079 known_variables: usize,
1080 has_storage_access: bool,
1081 has_step_notice: bool,
1082) -> String {
1083 if total_variables == 0 && !has_storage_access && !has_step_notice {
1084 return "Variables".to_string();
1085 }
1086
1087 let mut title = format!("Variables: {known_variables}/{total_variables}");
1088 if has_step_notice {
1089 title.push_str(" | Trace");
1090 }
1091 if has_storage_access {
1092 title.push_str(" | Storage");
1093 }
1094 title
1095}
1096
1097fn shortcut_lines() -> Vec<Line<'static>> {
1098 let dimmed = Style::new().add_modifier(Modifier::DIM);
1099 vec![
1100 Line::from(Span::styled(
1101 "[q] quit | [j/k] op | [a/s] jump | [c/C] call | [g/G] start/end | [p] PC | [o] offset",
1102 dimmed,
1103 )),
1104 Line::from(Span::styled(
1105 "[/] search | [:] command | [n/N] repeat | [l] layout | [b] buffer",
1106 dimmed,
1107 )),
1108 Line::from(Span::styled(
1109 "[t] labels | [m] decode | [h] help | [J/K] stack scroll | [ctrl+j/k] data scroll | ['<char>] breakpoint",
1110 dimmed,
1111 )),
1112 ]
1113}
1114
1115const COMMAND_PROMPT_HINT: &str = " Enter: run | Esc: cancel | help: command list";
1116
1117fn command_prompt_line(input: &str, width: u16) -> Line<'static> {
1118 let width = width as usize;
1119 let fixed_width = 2;
1120 let hint_width = text_width(COMMAND_PROMPT_HINT);
1121 let input_width = text_width(input);
1122 let include_hint = fixed_width + input_width + hint_width <= width;
1123 let input_width = if include_hint {
1124 width.saturating_sub(fixed_width + hint_width)
1125 } else {
1126 width.saturating_sub(fixed_width)
1127 };
1128
1129 let mut spans = vec![
1130 Span::styled(":", Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
1131 Span::raw(input_tail(input, input_width)),
1132 Span::styled("█", Style::new().fg(Color::Cyan)),
1133 ];
1134 if include_hint {
1135 spans.push(Span::styled(COMMAND_PROMPT_HINT, Style::new().add_modifier(Modifier::DIM)));
1136 }
1137 Line::from(spans)
1138}
1139
1140fn text_width(text: &str) -> usize {
1142 Span::raw(text).width()
1143}
1144
1145fn input_tail(input: &str, max_width: usize) -> String {
1146 if text_width(input) <= max_width {
1147 return input.to_string();
1148 }
1149 if max_width == 0 {
1150 return String::new();
1151 }
1152 if max_width == 1 {
1153 return "<".to_string();
1154 }
1155
1156 let tail_width = max_width - 1;
1159 let mut start = input.len();
1160 for (idx, _) in input.char_indices().rev() {
1161 if text_width(&input[idx..]) > tail_width {
1162 break;
1163 }
1164 start = idx;
1165 }
1166 format!("<{}", &input[start..])
1167}
1168
1169fn step_notice_title(line: &str) -> &'static str {
1170 if line.starts_with(PRECOMPILE_NOTICE_PREFIX) { "precompile call" } else { "decoded step" }
1171}
1172
1173fn step_notice_line(line: &str) -> Line<'static> {
1174 Line::from(Span::styled(line.to_string(), Style::new().fg(Color::Magenta)))
1175}
1176
1177fn scope_variable_line(variable: ScopeVariable) -> Line<'static> {
1178 let color = variable.kind.color();
1179 let mut spans = Vec::with_capacity(6);
1180 spans.push(Span::styled(variable.kind.label(), Style::new().fg(Color::Gray)));
1181 spans.push(Span::raw(" "));
1182 spans.push(Span::styled(variable.name, Style::new().fg(color).add_modifier(Modifier::BOLD)));
1183 spans.push(Span::raw(" = "));
1184 if let Some(value) = variable.value {
1185 spans.push(Span::styled(value, Style::new().fg(color)));
1186 } else {
1187 spans.push(Span::styled("<unavailable>", Style::new().fg(Color::Gray)));
1188 }
1189 Line::from(spans)
1190}
1191
1192fn storage_access_line(access: StorageAccess, label: Option<&str>) -> Line<'static> {
1193 let mut spans = vec![Span::styled(access.describe(), Style::new().fg(Color::Yellow))];
1194 if let Some(label) = label {
1195 spans.extend([
1196 Span::raw(" ("),
1197 Span::styled(label.to_string(), Color::Cyan),
1198 Span::raw(")"),
1199 ]);
1200 }
1201 Line::from(spans)
1202}
1203
1204fn storage_slot_lines(
1205 index: usize,
1206 index_width: usize,
1207 access: StorageAccess,
1208 label: Option<&str>,
1209 current: bool,
1210) -> [Line<'static>; 2] {
1211 let value_style = if current {
1212 Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
1213 } else {
1214 Style::new().fg(Color::White)
1215 };
1216 let prefix_width = index_width + 2;
1217 let mut slot_spans = vec![
1218 Span::styled(format!("{index:0index_width$}| "), Style::new().fg(Color::Gray)),
1219 Span::styled(access.op(), value_style),
1220 Span::raw(" slot "),
1221 Span::styled(hex_u256(access.slot()), value_style),
1222 ];
1223 if let Some(label) = label {
1224 slot_spans.extend([
1225 Span::raw(" ("),
1226 Span::styled(label.to_string(), Style::new().fg(Color::Cyan)),
1227 Span::raw(")"),
1228 ]);
1229 }
1230 [
1231 Line::from(slot_spans),
1232 Line::from(vec![
1233 Span::raw(" ".repeat(prefix_width)),
1234 Span::raw("value "),
1235 Span::styled(hex_u256(access.value()), value_style),
1236 ]),
1237 ]
1238}
1239
1240fn variable_name(variable: &DebugVariable, index: usize, fallback_prefix: &str) -> String {
1241 variable
1242 .name
1243 .as_deref()
1244 .filter(|name| !name.is_empty())
1245 .map(ToOwned::to_owned)
1246 .unwrap_or_else(|| format!("{fallback_prefix}{index}"))
1247}
1248
1249fn decoded_internal_name_matches(decoded_name: &str, scope: &DebugSourceScope) -> bool {
1250 if let Some((contract_name, function_name)) = decoded_name.rsplit_once("::") {
1251 return contract_name == scope.contract_name
1252 && decoded_function_matches(function_name, scope);
1253 }
1254 decoded_function_matches(decoded_name, scope)
1255}
1256
1257fn decoded_function_matches(decoded_name: &str, scope: &DebugSourceScope) -> bool {
1258 if decoded_name == scope.function_name {
1259 return true;
1260 }
1261 scope_function_signature(scope).as_deref().is_some_and(|signature| decoded_name == signature)
1262}
1263
1264fn decode_external_parameter_values(
1265 scope: &DebugSourceScope,
1266 calldata: &[u8],
1267) -> Option<Vec<String>> {
1268 let types = external_scope_parameter_types(scope, calldata)?;
1269
1270 decode_abi_sequence(&types, &calldata[4..])
1271}
1272
1273fn decode_external_return_values(
1274 scope: &DebugSourceScope,
1275 calldata: &[u8],
1276 returndata: &[u8],
1277) -> Option<Vec<String>> {
1278 external_scope_parameter_types(scope, calldata)?;
1279 let returns_src = scope.returns_src.as_deref()?;
1280 let returns = Parameters::parse(returns_src).ok()?;
1281 let types = resolved_types(&returns)?;
1282 decode_abi_sequence(&types, returndata)
1283}
1284
1285fn external_scope_parameter_types(
1286 scope: &DebugSourceScope,
1287 calldata: &[u8],
1288) -> Option<Vec<DynSolType>> {
1289 if calldata.len() < 4 {
1290 return None;
1291 }
1292
1293 let parameters = Parameters::parse(&scope.parameters_src).ok()?;
1294 let types = resolved_types(¶meters)?;
1295 let selector = function_selector(&scope.function_name, &types);
1296 if calldata.get(..4)? != selector.as_slice() {
1297 return None;
1298 }
1299
1300 Some(types)
1301}
1302
1303fn resolved_types(parameters: &Parameters<'_>) -> Option<Vec<DynSolType>> {
1304 parameters.params.iter().map(|param| param.resolve().ok()).collect()
1305}
1306
1307fn scope_function_signature(scope: &DebugSourceScope) -> Option<String> {
1308 let parameters = Parameters::parse(&scope.parameters_src).ok()?;
1309 let types = resolved_types(¶meters)?;
1310 Some(function_signature(&scope.function_name, &types))
1311}
1312
1313fn function_selector(function_name: &str, types: &[DynSolType]) -> [u8; 4] {
1314 let signature = function_signature(function_name, types);
1315 keccak256(signature.as_bytes())[..4].try_into().unwrap()
1316}
1317
1318fn decode_abi_sequence(types: &[DynSolType], data: &[u8]) -> Option<Vec<String>> {
1319 if types.is_empty() {
1320 return Some(Vec::new());
1321 }
1322
1323 let value = DynSolType::Tuple(types.to_vec()).abi_decode_sequence(data).ok()?;
1324 let values = value.as_fixed_seq()?;
1325 Some(values.iter().map(format_token).collect())
1326}
1327
1328fn op_list_title(
1329 address: &Address,
1330 pc: usize,
1331 gas_remaining: u64,
1332 call_gas_used: u64,
1333 gas_refund_counter: u64,
1334 stats: Option<DebuggerStats>,
1335) -> String {
1336 let address = full_checksum_address(address);
1337 let mut title = format!(
1338 "address: {address} | pc: 0x{pc:x} ({pc}) | gasLeft: {gas_remaining} | \
1339 callGasUsed: {call_gas_used} | gasRefund: {gas_refund_counter}"
1340 );
1341
1342 if let Some(stats) = stats {
1343 write!(
1344 title,
1345 " | sessionTraceGasUsed: {} | sessionSubcalls: {}",
1346 stats.session_trace_gas_used, stats.session_subcalls
1347 )
1348 .unwrap();
1349 }
1350
1351 title
1352}
1353
1354fn full_checksum_address(address: &Address) -> String {
1355 address.to_string()
1356}
1357
1358fn stack_item_line(
1359 i: usize,
1360 min_len: usize,
1361 stack_item: &U256,
1362 param: Option<&OpcodeParam>,
1363 stack_labels: bool,
1364) -> Line<'static> {
1365 let value_style =
1366 if param.is_some() { Style::new().fg(Color::Cyan) } else { Style::new().fg(Color::White) };
1367 let mut spans = Vec::with_capacity(1 + 32 * 2 + 5);
1368
1369 spans.push(Span::styled(format!("{i:0min_len$}| "), Style::new().fg(Color::White)));
1371
1372 hex_bytes_spans(&stack_item.to_be_bytes::<32>(), &mut spans, |_, _| value_style);
1374
1375 spans.push(Span::raw(" | "));
1376 spans.push(Span::styled(stack_item.to_string(), value_style));
1377
1378 if stack_labels && let Some(param) = param {
1379 spans.push(Span::raw(" | "));
1380 spans.push(Span::raw(param.name));
1381 }
1382
1383 spans.push(Span::raw("\n"));
1384
1385 Line::from(spans)
1386}
1387
1388struct SourceLines<'a> {
1390 lines: Vec<Line<'a>>,
1391 start_line: usize,
1392 max_line_num: usize,
1393}
1394
1395impl<'a> SourceLines<'a> {
1396 fn new(start_line: usize, end_line: usize) -> Self {
1397 Self { lines: Vec::new(), start_line, max_line_num: decimal_digits(end_line) }
1398 }
1399
1400 fn push(&mut self, line_number_style: Style, line: &'a str, line_style: Style) {
1401 self.push_raw(line_number_style, &[Span::styled(line, line_style)]);
1402 }
1403
1404 fn push_raw(&mut self, line_number_style: Style, spans: &[Span<'a>]) {
1405 let mut line_spans = Vec::with_capacity(4);
1406
1407 let line_number = format!(
1408 "{number: >width$} ",
1409 number = self.start_line + self.lines.len() + 1,
1410 width = self.max_line_num
1411 );
1412 line_spans.push(Span::styled(line_number, line_number_style));
1413
1414 line_spans.push(Span::raw(" "));
1416
1417 line_spans.extend_from_slice(spans);
1418
1419 self.lines.push(Line::from(line_spans));
1420 }
1421}
1422
1423fn hex_bytes_spans(bytes: &[u8], spans: &mut Vec<Span<'_>>, f: impl Fn(usize, u8) -> Style) {
1424 for (i, &byte) in bytes.iter().enumerate() {
1425 if i > 0 {
1426 spans.push(Span::raw(" "));
1427 }
1428 spans.push(Span::styled(alloy_primitives::hex::encode([byte]), f(i, byte)));
1429 }
1430}
1431
1432fn decimal_digits(n: usize) -> usize {
1436 n.checked_ilog10().unwrap_or(0) as usize + 1
1437}
1438
1439fn hex_digits(n: usize) -> usize {
1443 n.checked_ilog(16).unwrap_or(0) as usize + 1
1444}
1445
1446#[cfg(test)]
1447mod tests {
1448 use super::{Prompt, TUIContext};
1449 use crate::{
1450 DebugNode, DebuggerLayout,
1451 debugger::{DebuggerContext, DebuggerStats},
1452 op::OpcodeParam,
1453 };
1454 use alloy_dyn_abi::parser::Parameters;
1455 use alloy_primitives::{Address, Bytes, U256, address};
1456 use foundry_evm_core::{Breakpoints, buffer::BufferKind};
1457 use foundry_evm_traces::debug::{ContractSources, DebugSourceScope, DebugVariable};
1458 use ratatui::{
1459 Terminal,
1460 backend::TestBackend,
1461 layout::Rect,
1462 style::{Color, Style},
1463 text::Line,
1464 };
1465 use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
1466 use revm_inspectors::tracing::types::{
1467 CallKind, CallTraceStep, DecodedCallData, DecodedCallTrace, DecodedInternalCall,
1468 DecodedTraceStep, StorageChange, StorageChangeReason,
1469 };
1470
1471 fn line_text(line: &Line<'_>) -> String {
1472 line.spans.iter().map(|span| span.content.as_ref()).collect()
1473 }
1474
1475 fn scope(function_name: &str, parameters_src: &str) -> DebugSourceScope {
1476 DebugSourceScope {
1477 contract_name: "DebugMe".to_string(),
1478 function_name: function_name.to_string(),
1479 range: 0..100,
1480 body_range: 10..90,
1481 parameters_src: parameters_src.to_string(),
1482 returns_src: None,
1483 parameters: Vec::new(),
1484 returns: Vec::new(),
1485 locals: Vec::new(),
1486 }
1487 }
1488
1489 fn scope_with_returns(
1490 function_name: &str,
1491 parameters_src: &str,
1492 returns_src: &str,
1493 ) -> DebugSourceScope {
1494 DebugSourceScope {
1495 returns_src: Some(returns_src.to_string()),
1496 ..scope(function_name, parameters_src)
1497 }
1498 }
1499
1500 fn trace_step(stack: Vec<U256>) -> CallTraceStep {
1501 CallTraceStep {
1502 pc: 0,
1503 op: OpCode::STOP,
1504 stack: Some(stack.into_boxed_slice()),
1505 push_stack: None,
1506 memory: None,
1507 returndata: Bytes::new(),
1508 gas_remaining: 0,
1509 gas_refund_counter: 0,
1510 gas_used: 0,
1511 gas_cost: 0,
1512 state_gas_cost: None,
1513 state_gas_reservoir: None,
1514 state_gas_spent: 0,
1515 storage_change: None,
1516 status: Some(InstructionResult::Stop),
1517 immediate_bytes: None,
1518 decoded: None,
1519 }
1520 }
1521
1522 fn internal_call_step(end_step: usize, return_data: Vec<String>) -> CallTraceStep {
1523 internal_call_step_named("DebugMe::foo", end_step, Some(Vec::new()), Some(return_data))
1524 }
1525
1526 fn internal_call_step_named(
1527 func_name: &str,
1528 end_step: usize,
1529 args: Option<Vec<String>>,
1530 return_data: Option<Vec<String>>,
1531 ) -> CallTraceStep {
1532 let mut step = trace_step(Vec::new());
1533 step.decoded = Some(Box::new(DecodedTraceStep::InternalCall(
1534 DecodedInternalCall { func_name: func_name.to_string(), args, return_data },
1535 end_step,
1536 )));
1537 step
1538 }
1539
1540 fn internal_call_step_without_args(end_step: usize) -> CallTraceStep {
1541 let mut step = trace_step(Vec::new());
1542 step.decoded = Some(Box::new(DecodedTraceStep::InternalCall(
1543 DecodedInternalCall {
1544 func_name: "DebugMe::foo".to_string(),
1545 args: None,
1546 return_data: None,
1547 },
1548 end_step,
1549 )));
1550 step
1551 }
1552
1553 fn debug_node(
1554 trace_node_idx: usize,
1555 step_offset: usize,
1556 steps: Vec<CallTraceStep>,
1557 ) -> DebugNode {
1558 let mut node = DebugNode::new(Address::ZERO, CallKind::Call, steps, Bytes::new(), 0, None);
1559 node.trace_node_idx = trace_node_idx;
1560 node.step_offset = step_offset;
1561 node
1562 }
1563
1564 fn context_with_arena(arena: Vec<DebugNode>) -> DebuggerContext {
1565 DebuggerContext {
1566 debug_arena: arena,
1567 stats: None,
1568 identified_contracts: Default::default(),
1569 slot_identifiers: None,
1570 contracts_sources: ContractSources::default(),
1571 breakpoints: Breakpoints::default(),
1572 layout: Default::default(),
1573 }
1574 }
1575
1576 fn abi_word(value: U256) -> [u8; 32] {
1577 value.to_be_bytes::<32>()
1578 }
1579
1580 #[test]
1581 fn draw_buffer_handles_missing_memory_snapshot() {
1582 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1583 let tui = TUIContext::new(&mut context);
1584 let backend = TestBackend::new(80, 4);
1585 let mut terminal = Terminal::new(backend).unwrap();
1586
1587 terminal.draw(|f| tui.draw_buffer(f, Rect::new(0, 0, 80, 4))).unwrap();
1588
1589 let screen = terminal
1590 .backend()
1591 .buffer()
1592 .content()
1593 .iter()
1594 .map(|cell| cell.symbol())
1595 .collect::<String>();
1596 assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1597 }
1598
1599 #[test]
1600 fn draw_buffer_does_not_overflow_on_saturated_stack_offsets() {
1601 let stack = vec![U256::MAX, U256::MAX];
1607 let mut step = trace_step(stack);
1608 step.op = OpCode::LOG1;
1609
1610 let mut node = debug_node(0, 0, vec![step]);
1611 node.calldata = Bytes::from(vec![0u8; 64]);
1612 let mut context = context_with_arena(vec![node]);
1613 let mut tui = TUIContext::new(&mut context);
1614 tui.active_buffer = BufferKind::Calldata;
1619
1620 let backend = TestBackend::new(80, 6);
1621 let mut terminal = Terminal::new(backend).unwrap();
1622
1623 terminal.draw(|f| tui.draw_buffer(f, Rect::new(0, 0, 80, 6))).unwrap();
1625 }
1626
1627 #[test]
1628 fn opcode_list_draws_visible_window() {
1629 let steps = (0..64)
1630 .chain([0x100])
1631 .map(|pc| {
1632 let mut step = trace_step(Vec::new());
1633 step.pc = pc;
1634 step.op = OpCode::ADD;
1635 step
1636 })
1637 .collect();
1638 let mut context = context_with_arena(vec![debug_node(0, 0, steps)]);
1639 let mut tui = TUIContext::new(&mut context);
1640 tui.init();
1641 tui.current_step = 50;
1642 let backend = TestBackend::new(80, 20);
1643 let mut terminal = Terminal::new(backend).unwrap();
1644
1645 terminal.draw(|f| tui.draw_op_list(f, Rect::new(0, 0, 80, 20))).unwrap();
1646
1647 let lines = terminal
1648 .backend()
1649 .buffer()
1650 .content()
1651 .chunks(80)
1652 .map(|cells| cells.iter().map(|cell| cell.symbol()).collect::<String>())
1653 .collect::<Vec<_>>();
1654 assert!(lines[1].contains("022|ADD"));
1655 assert!(lines[17].contains("▶032|ADD"));
1656 assert!(lines[18].contains("033|ADD"));
1657
1658 let backend = TestBackend::new(80, 3);
1659 let mut terminal = Terminal::new(backend).unwrap();
1660 terminal.draw(|f| tui.draw_op_list(f, Rect::new(0, 0, 80, 3))).unwrap();
1661 let screen = terminal
1662 .backend()
1663 .buffer()
1664 .content()
1665 .iter()
1666 .map(|cell| cell.symbol())
1667 .collect::<String>();
1668 assert!(screen.contains("▶032|ADD"));
1669 }
1670
1671 #[test]
1672 fn storage_explorer_draws_accessed_slots() {
1673 let mut first = trace_step(Vec::new());
1674 first.storage_change = Some(Box::new(StorageChange {
1675 key: U256::ZERO,
1676 value: U256::from(42),
1677 had_value: None,
1678 reason: StorageChangeReason::SSTORE,
1679 }));
1680 let mut second = trace_step(Vec::new());
1681 second.storage_change = Some(Box::new(StorageChange {
1682 key: U256::from(1),
1683 value: U256::from(0xbeef),
1684 had_value: None,
1685 reason: StorageChangeReason::SSTORE,
1686 }));
1687 let mut latest = trace_step(Vec::new());
1688 latest.storage_change = Some(Box::new(StorageChange {
1689 key: U256::ZERO,
1690 value: U256::from(43),
1691 had_value: Some(U256::from(42)),
1692 reason: StorageChangeReason::SSTORE,
1693 }));
1694 let mut context = context_with_arena(vec![debug_node(0, 0, vec![first, second, latest])]);
1695 let mut tui = TUIContext::new(&mut context);
1696 tui.current_step = 2;
1697 let backend = TestBackend::new(100, 6);
1698 let mut terminal = Terminal::new(backend).unwrap();
1699
1700 terminal
1701 .draw(|f| tui.draw_storage(f, Rect::new(0, 0, 100, 6), super::StorageSpace::Persistent))
1702 .unwrap();
1703
1704 let screen = terminal
1705 .backend()
1706 .buffer()
1707 .content()
1708 .iter()
1709 .map(|cell| cell.symbol())
1710 .collect::<String>();
1711 assert!(screen.contains("Storage: 2 accessed slots"));
1712 assert!(screen.contains("SSTORE slot 0x0"));
1713 assert!(screen.contains("value 0x2b"));
1714 assert!(screen.contains("SSTORE slot 0x1"));
1715 assert!(screen.contains("value 0xbeef"));
1716 }
1717
1718 #[test]
1719 fn hidden_source_pane_omits_source_panel() {
1720 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1721 context.layout = DebuggerLayout::Horizontal;
1722 let mut tui = TUIContext::new(&mut context);
1723 tui.init();
1724 tui.show_source = false;
1725 let backend = TestBackend::new(220, 30);
1726 let mut terminal = Terminal::new(backend).unwrap();
1727
1728 terminal.draw(|f| tui.draw_layout(f)).unwrap();
1729
1730 let screen = terminal
1731 .backend()
1732 .buffer()
1733 .content()
1734 .iter()
1735 .map(|cell| cell.symbol())
1736 .collect::<String>();
1737 assert!(!screen.contains("Contract call"));
1738 assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1739 }
1740
1741 #[test]
1742 fn hidden_opcodes_pane_omits_opcode_panel() {
1743 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1744 context.layout = DebuggerLayout::Horizontal;
1745 let mut tui = TUIContext::new(&mut context);
1746 tui.init();
1747 tui.show_opcodes = false;
1748 let backend = TestBackend::new(220, 30);
1749 let mut terminal = Terminal::new(backend).unwrap();
1750
1751 terminal.draw(|f| tui.draw_layout(f)).unwrap();
1752
1753 let screen = terminal
1754 .backend()
1755 .buffer()
1756 .content()
1757 .iter()
1758 .map(|cell| cell.symbol())
1759 .collect::<String>();
1760 assert!(!screen.contains("address:"));
1761 assert!(screen.contains("Contract call"));
1762 assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1763 }
1764
1765 #[test]
1766 fn hidden_variables_pane_omits_variables_panel() {
1767 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1768 context.layout = DebuggerLayout::Horizontal;
1769 let mut tui = TUIContext::new(&mut context);
1770 tui.init();
1771 tui.show_variables = false;
1772 let backend = TestBackend::new(220, 30);
1773 let mut terminal = Terminal::new(backend).unwrap();
1774
1775 terminal.draw(|f| tui.draw_layout(f)).unwrap();
1776
1777 let screen = terminal
1778 .backend()
1779 .buffer()
1780 .content()
1781 .iter()
1782 .map(|cell| cell.symbol())
1783 .collect::<String>();
1784 assert!(!screen.contains("Variables"));
1785 assert!(screen.contains("Stack"));
1786 assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1787 }
1788
1789 #[test]
1790 fn hidden_stack_pane_omits_stack_panel() {
1791 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1792 context.layout = DebuggerLayout::Horizontal;
1793 let mut tui = TUIContext::new(&mut context);
1794 tui.init();
1795 tui.show_stack = false;
1796 let backend = TestBackend::new(220, 30);
1797 let mut terminal = Terminal::new(backend).unwrap();
1798
1799 terminal.draw(|f| tui.draw_layout(f)).unwrap();
1800
1801 let screen = terminal
1802 .backend()
1803 .buffer()
1804 .content()
1805 .iter()
1806 .map(|cell| cell.symbol())
1807 .collect::<String>();
1808 assert!(!screen.contains("Stack:"));
1809 assert!(screen.contains("Variables"));
1810 assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1811 }
1812
1813 #[test]
1814 fn hidden_data_pane_omits_data_panel() {
1815 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1816 context.layout = DebuggerLayout::Horizontal;
1817 let mut tui = TUIContext::new(&mut context);
1818 tui.init();
1819 tui.show_opcodes = false;
1820 tui.show_variables = false;
1821 tui.show_stack = false;
1822 tui.show_data = false;
1823 let backend = TestBackend::new(80, 30);
1824 let mut terminal = Terminal::new(backend).unwrap();
1825
1826 terminal.draw(|f| tui.draw_layout(f)).unwrap();
1827
1828 let screen = terminal
1829 .backend()
1830 .buffer()
1831 .content()
1832 .iter()
1833 .map(|cell| cell.symbol())
1834 .collect::<String>();
1835 assert!(!screen.contains("Memory (max expansion: 0 bytes)"));
1836 assert!(screen.contains("Contract call"));
1837 }
1838
1839 #[test]
1840 fn command_prompt_draws_in_bordered_block() {
1841 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1842 let mut tui = TUIContext::new(&mut context);
1843 tui.prompt = Some(Prompt::Command("pc 0".to_string()));
1844 let backend = TestBackend::new(120, 6);
1845 let mut terminal = Terminal::new(backend).unwrap();
1846
1847 terminal.draw(|f| tui.draw_footer(f, Rect::new(0, 0, 120, 6))).unwrap();
1848
1849 let screen = terminal
1850 .backend()
1851 .buffer()
1852 .content()
1853 .iter()
1854 .map(|cell| cell.symbol())
1855 .collect::<String>();
1856 assert!(screen.contains("Command"));
1857 assert!(screen.contains(":pc 0"));
1858 assert!(screen.contains("Enter: run"));
1859 assert!(screen.contains("[:] command"));
1860 }
1861
1862 #[test]
1863 fn command_prompt_line_clips_long_input_to_tail() {
1864 let input = "continue 0123456789abcdef";
1865 let line = super::command_prompt_line(input, 12);
1866 let text = line_text(&line);
1867
1868 assert!(line.width() <= 12);
1869 assert_eq!(text, ":<789abcdef█");
1870 assert!(!text.contains("Enter: run"));
1871 }
1872
1873 #[test]
1874 fn command_prompt_line_clips_wide_unicode_to_width() {
1875 let input = "界界界界界界界界";
1878 let line = super::command_prompt_line(input, 8);
1879
1880 assert!(line.width() <= 8);
1881 }
1882
1883 #[test]
1884 fn decode_external_parameter_values_decodes_named_params() {
1885 let scope = scope("foo", "(uint256 amount, bool ok)");
1886 let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1887 let types = super::resolved_types(¶meters).unwrap();
1888 let mut calldata = Vec::new();
1889 calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1890 calldata.extend_from_slice(&abi_word(U256::from(42)));
1891 calldata.extend_from_slice(&abi_word(U256::from(1)));
1892
1893 let values = super::decode_external_parameter_values(&scope, &calldata).unwrap();
1894
1895 assert_eq!(values, ["42", "true"]);
1896 }
1897
1898 #[test]
1899 fn decode_external_parameter_values_rejects_selector_mismatch() {
1900 let scope = scope("foo", "(uint256 amount)");
1901 let parameters = Parameters::parse("(uint256 amount)").unwrap();
1902 let types = super::resolved_types(¶meters).unwrap();
1903 let mut calldata = Vec::new();
1904 calldata.extend_from_slice(&super::function_selector("bar", &types));
1905 calldata.extend_from_slice(&abi_word(U256::from(42)));
1906
1907 assert_eq!(super::decode_external_parameter_values(&scope, &calldata), None);
1908 }
1909
1910 #[test]
1911 fn decode_external_return_values_decodes_named_returns() {
1912 let scope = scope_with_returns("foo", "(uint256 amount)", "(uint256 total, bool ok)");
1913 let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1914 let types = super::resolved_types(¶meters).unwrap();
1915 let mut calldata = Vec::new();
1916 calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1917 calldata.extend_from_slice(&abi_word(U256::from(42)));
1918 let mut returndata = Vec::new();
1919 returndata.extend_from_slice(&abi_word(U256::from(99)));
1920 returndata.extend_from_slice(&abi_word(U256::from(1)));
1921
1922 let values = super::decode_external_return_values(&scope, &calldata, &returndata).unwrap();
1923
1924 assert_eq!(values, ["99", "true"]);
1925 }
1926
1927 #[test]
1928 fn decode_return_values_uses_external_output_at_frame_end() {
1929 let scope = scope_with_returns("foo", "()", "(uint256 total)");
1930 let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1931 let types = super::resolved_types(¶meters).unwrap();
1932 let mut calldata = Vec::new();
1933 calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1934 let mut node = debug_node(
1935 0,
1936 0,
1937 vec![trace_step(Vec::new()), {
1938 let mut step = trace_step(Vec::new());
1939 step.op = OpCode::RETURN;
1940 step.status = Some(InstructionResult::Return);
1941 step
1942 }],
1943 );
1944 node.calldata = Bytes::from(calldata);
1945 node.returndata = Bytes::from(abi_word(U256::from(123)).to_vec());
1946 let mut context = context_with_arena(vec![node]);
1947 let mut tui = TUIContext::new(&mut context);
1948 tui.current_step = 1;
1949
1950 assert_eq!(tui.decode_return_values(&scope), Some(vec!["123".to_string()]));
1951 }
1952
1953 #[test]
1954 fn decode_return_values_waits_for_external_frame_end() {
1955 let scope = scope_with_returns("foo", "()", "(uint256 total)");
1956 let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1957 let types = super::resolved_types(¶meters).unwrap();
1958 let mut calldata = Vec::new();
1959 calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1960 let mut node = debug_node(0, 0, vec![trace_step(Vec::new()), trace_step(Vec::new())]);
1961 node.calldata = Bytes::from(calldata);
1962 node.returndata = Bytes::from(abi_word(U256::from(123)).to_vec());
1963 let mut context = context_with_arena(vec![node]);
1964 let mut tui = TUIContext::new(&mut context);
1965
1966 assert_eq!(tui.decode_return_values(&scope), None);
1967 }
1968
1969 #[test]
1970 fn scope_function_signature_includes_resolved_parameter_types() {
1971 let scope = scope("foo", "(uint256 amount, bool ok)");
1972
1973 assert_eq!(super::scope_function_signature(&scope).as_deref(), Some("foo(uint256,bool)"));
1974 }
1975
1976 #[test]
1977 fn decode_parameter_values_rejects_decoded_call_data_for_wrong_overload() {
1978 let mut node = debug_node(0, 0, vec![trace_step(Vec::new())]);
1979 node.decoded = Some(Box::new(DecodedCallTrace {
1980 call_data: Some(DecodedCallData {
1981 signature: "foo(address)".to_string(),
1982 args: vec!["0x000000000000000000000000000000000000002a".to_string()],
1983 }),
1984 ..Default::default()
1985 }));
1986 let mut context = context_with_arena(vec![node]);
1987 let mut tui = TUIContext::new(&mut context);
1988
1989 assert_eq!(tui.decode_parameter_values(&scope("foo", "(uint256 amount)")), None);
1990 }
1991
1992 #[test]
1993 fn decode_step_parameters_reads_static_values_from_stack() {
1994 let step = trace_step(vec![U256::from(42), U256::from(1)]);
1995 let parameters = Parameters::parse("(uint256 amount, bool ok)").unwrap();
1996 let values = super::decode_step_parameters(¶meters, &step, None).unwrap();
1997
1998 assert_eq!(values, ["42", "true"]);
1999 }
2000
2001 #[test]
2002 fn decode_internal_parameter_values_uses_absolute_entry_step() {
2003 let mut context = context_with_arena(vec![
2004 debug_node(0, 0, vec![internal_call_step_without_args(2)]),
2005 debug_node(1, 0, vec![trace_step(Vec::new())]),
2006 debug_node(0, 1, vec![trace_step(vec![U256::from(42)])]),
2007 ]);
2008 let mut tui = TUIContext::new(&mut context);
2009 tui.draw_memory.inner_call_index = 2;
2010
2011 assert_eq!(
2012 tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")),
2013 Some(vec!["42".to_string()])
2014 );
2015 }
2016
2017 #[test]
2018 fn decode_internal_parameter_values_passes_calldata_to_fallback_decoder() {
2019 let digest = U256::from(0x1234);
2020 let offset = 0x44;
2021 let mut calldata = vec![0; offset];
2022 calldata.extend_from_slice(&[0x11, 0x22, 0x33]);
2023
2024 let mut entry_node =
2025 debug_node(0, 1, vec![trace_step(vec![digest, U256::from(offset), U256::from(3)])]);
2026 entry_node.calldata = Bytes::from(calldata);
2027
2028 let mut context = context_with_arena(vec![
2029 debug_node(0, 0, vec![internal_call_step_without_args(2)]),
2030 debug_node(1, 0, vec![trace_step(Vec::new())]),
2031 entry_node,
2032 ]);
2033 let mut tui = TUIContext::new(&mut context);
2034 tui.draw_memory.inner_call_index = 2;
2035
2036 assert_eq!(
2037 tui.decode_internal_parameter_values(&scope(
2038 "foo",
2039 "(bytes32 digest, bytes calldata signature)"
2040 )),
2041 Some(vec![
2042 "0x0000000000000000000000000000000000000000000000000000000000001234".to_string(),
2043 "0x112233".to_string(),
2044 ])
2045 );
2046 }
2047
2048 #[test]
2049 fn decode_internal_parameter_values_accepts_matching_overload_args() {
2050 let mut context = context_with_arena(vec![debug_node(
2051 0,
2052 0,
2053 vec![internal_call_step_named(
2054 "DebugMe::foo(uint256)",
2055 2,
2056 Some(vec!["42".to_string()]),
2057 None,
2058 )],
2059 )]);
2060 let mut tui = TUIContext::new(&mut context);
2061
2062 assert_eq!(
2063 tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")),
2064 Some(vec!["42".to_string()])
2065 );
2066 }
2067
2068 #[test]
2069 fn decode_internal_parameter_values_rejects_wrong_overload_args() {
2070 let mut context = context_with_arena(vec![debug_node(
2071 0,
2072 0,
2073 vec![internal_call_step_named(
2074 "DebugMe::foo(address)",
2075 2,
2076 Some(vec!["0x000000000000000000000000000000000000002a".to_string()]),
2077 None,
2078 )],
2079 )]);
2080 let mut tui = TUIContext::new(&mut context);
2081
2082 assert_eq!(tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")), None);
2083 }
2084
2085 #[test]
2086 fn decode_return_values_uses_absolute_internal_call_end_step() {
2087 let mut context = context_with_arena(vec![debug_node(
2088 0,
2089 3,
2090 vec![internal_call_step(4, vec!["99".to_string()]), trace_step(Vec::new())],
2091 )]);
2092 let mut tui = TUIContext::new(&mut context);
2093 tui.current_step = 1;
2094
2095 assert_eq!(tui.decode_return_values(&scope("foo", "()")), Some(vec!["99".to_string()]));
2096 }
2097
2098 #[test]
2099 fn decode_return_values_finds_internal_call_split_by_child_node() {
2100 let mut context = context_with_arena(vec![
2101 debug_node(0, 0, vec![internal_call_step(2, vec!["7".to_string()])]),
2102 debug_node(1, 0, vec![trace_step(Vec::new())]),
2103 debug_node(0, 2, vec![trace_step(Vec::new())]),
2104 ]);
2105 let mut tui = TUIContext::new(&mut context);
2106 tui.draw_memory.inner_call_index = 2;
2107
2108 assert_eq!(tui.decode_return_values(&scope("foo", "()")), Some(vec!["7".to_string()]));
2109 }
2110
2111 #[test]
2112 fn decode_return_values_rejects_wrong_overload() {
2113 let mut context = context_with_arena(vec![debug_node(
2114 0,
2115 0,
2116 vec![
2117 internal_call_step_named(
2118 "DebugMe::foo(address)",
2119 1,
2120 None,
2121 Some(vec!["99".to_string()]),
2122 ),
2123 trace_step(Vec::new()),
2124 ],
2125 )]);
2126 let mut tui = TUIContext::new(&mut context);
2127 tui.current_step = 1;
2128
2129 assert_eq!(tui.decode_return_values(&scope("foo", "(uint256 amount)")), None);
2130 }
2131
2132 #[test]
2133 fn active_internal_call_caches_by_current_node_and_step() {
2134 let mut context = context_with_arena(vec![debug_node(
2135 0,
2136 0,
2137 vec![internal_call_step(2, vec!["1".to_string()]), trace_step(Vec::new())],
2138 )]);
2139 let mut tui = TUIContext::new(&mut context);
2140
2141 assert!(tui.active_internal_call().is_some());
2142 let cache = tui.draw_memory.active_internal_call;
2143 assert!(cache.and_then(|cache| cache.location).is_some());
2144
2145 assert!(tui.active_internal_call().is_some());
2146 assert_eq!(tui.draw_memory.active_internal_call, cache);
2147
2148 tui.current_step = 1;
2149 assert!(tui.active_internal_call().is_some());
2150 assert_ne!(tui.draw_memory.active_internal_call, cache);
2151 }
2152
2153 #[test]
2154 fn decoded_internal_name_matches_exact_contract_and_function() {
2155 let scope = scope("foo", "()");
2156
2157 assert!(super::decoded_internal_name_matches("DebugMe::foo", &scope));
2158 assert!(!super::decoded_internal_name_matches("DebugMe::barfoo", &scope));
2159 assert!(!super::decoded_internal_name_matches("Other::foo", &scope));
2160 }
2161
2162 #[test]
2163 fn decoded_internal_name_matches_canonical_signature_for_overloads() {
2164 let scope = scope("foo", "(uint256 amount)");
2165
2166 assert!(super::decoded_internal_name_matches("DebugMe::foo(uint256)", &scope));
2167 assert!(!super::decoded_internal_name_matches("DebugMe::foo(address)", &scope));
2168 assert!(!super::decoded_internal_name_matches("Other::foo(uint256)", &scope));
2169 }
2170
2171 #[test]
2172 fn scope_variable_line_marks_unavailable_locals() {
2173 let variable = super::ScopeVariable {
2174 kind: super::ScopeVariableKind::Local,
2175 name: "sum".to_string(),
2176 value: None,
2177 };
2178
2179 assert_eq!(line_text(&super::scope_variable_line(variable)), "local sum = <unavailable>");
2180 }
2181
2182 #[test]
2183 fn storage_lines_format_sload_and_label() {
2184 let mut step = trace_step(Vec::new());
2185 step.storage_change = Some(Box::new(StorageChange {
2186 key: U256::from(1),
2187 value: U256::from(42),
2188 had_value: None,
2189 reason: StorageChangeReason::SLOAD,
2190 }));
2191 let steps = [step];
2192 let access = super::storage_access_at(&steps, 0).unwrap();
2193
2194 assert_eq!(
2195 line_text(&super::storage_access_line(access, Some("count"))),
2196 "storage SLOAD slot 0x1 = 0x2a (count)"
2197 );
2198 assert_eq!(
2199 line_text(&super::storage_slot_lines(0, 2, access, Some("count"), false)[0]),
2200 "00| SLOAD slot 0x1 (count)"
2201 );
2202 }
2203
2204 #[test]
2205 fn storage_access_line_formats_sstore_with_previous_value() {
2206 let mut step = trace_step(Vec::new());
2207 step.storage_change = Some(Box::new(StorageChange {
2208 key: U256::from(1),
2209 value: U256::from(42),
2210 had_value: Some(U256::from(7)),
2211 reason: StorageChangeReason::SSTORE,
2212 }));
2213 let steps = [step];
2214 let access = super::storage_access_at(&steps, 0).unwrap();
2215
2216 assert_eq!(
2217 line_text(&super::storage_access_line(access, None)),
2218 "storage SSTORE slot 0x1: 0x7 -> 0x2a"
2219 );
2220 }
2221
2222 #[test]
2223 fn current_storage_access_line_uses_next_stack_snapshot_for_warm_sload() {
2224 let mut step = trace_step(vec![U256::from(1)]);
2225 step.op = OpCode::SLOAD;
2226 step.storage_change = None;
2227 let next_step = trace_step(vec![U256::from(42)]);
2228 let mut context = context_with_arena(vec![debug_node(0, 0, vec![step, next_step])]);
2229 let tui = TUIContext::new(&mut context);
2230
2231 assert_eq!(
2232 line_text(&tui.current_storage_access_line().unwrap()),
2233 "storage SLOAD slot 0x1 = 0x2a"
2234 );
2235 }
2236
2237 #[test]
2238 fn current_storage_access_line_uses_next_stack_snapshot_for_tload() {
2239 let mut step = trace_step(vec![U256::from(1)]);
2240 step.op = OpCode::TLOAD;
2241 let next_step = trace_step(vec![U256::from(42)]);
2242 let mut context = context_with_arena(vec![debug_node(0, 0, vec![step, next_step])]);
2243 let tui = TUIContext::new(&mut context);
2244
2245 assert_eq!(
2246 line_text(&tui.current_storage_access_line().unwrap()),
2247 "transient storage TLOAD slot 0x1 = 0x2a"
2248 );
2249 }
2250
2251 #[test]
2252 fn variable_name_falls_back_for_unnamed_values() {
2253 let variable = DebugVariable { name: None, declaration: 0..1, scope: 0..2 };
2254
2255 assert_eq!(super::variable_name(&variable, 2, "arg"), "arg2");
2256 }
2257
2258 #[test]
2259 fn current_step_notice_text_reads_decoded_line_steps() {
2260 let mut step = trace_step(Vec::new());
2261 step.decoded = Some(Box::new(DecodedTraceStep::Line(
2262 "precompile: PRECOMPILES::sha256(0x68656c6c6f)".to_string(),
2263 )));
2264 let mut context = context_with_arena(vec![debug_node(0, 0, vec![step])]);
2265 let tui = TUIContext::new(&mut context);
2266
2267 assert_eq!(
2268 tui.current_step_notice_text(),
2269 Some("precompile: PRECOMPILES::sha256(0x68656c6c6f)")
2270 );
2271 }
2272
2273 #[test]
2274 fn source_pane_title_includes_precompile_notice_before_source() {
2275 assert_eq!(
2276 super::source_pane_title(
2277 "Contract call",
2278 Some("test/Precompile.t.sol"),
2279 Some("precompile call")
2280 ),
2281 "Contract call | precompile call | test/Precompile.t.sol "
2282 );
2283 }
2284
2285 #[test]
2286 fn variables_title_tracks_decoded_step_notices() {
2287 assert_eq!(super::variables_title(0, 0, false, false), "Variables");
2288 assert_eq!(super::variables_title(0, 0, false, true), "Variables: 0/0 | Trace");
2289 assert_eq!(super::variables_title(2, 1, true, true), "Variables: 1/2 | Trace | Storage");
2290 }
2291
2292 #[test]
2293 fn step_notice_line_highlights_precompile_clue() {
2294 let line = super::step_notice_line("precompile: PRECOMPILES::sha256(0x68656c6c6f)");
2295
2296 assert_eq!(line_text(&line), "precompile: PRECOMPILES::sha256(0x68656c6c6f)");
2297 assert_eq!(line.spans[0].style, Style::new().fg(Color::Magenta));
2298 assert_eq!(super::step_notice_title(&line_text(&line)), "precompile call");
2299 }
2300
2301 #[test]
2302 fn op_list_title_includes_gas_and_subcall_stats() {
2303 let stats = DebuggerStats { session_trace_gas_used: 789_012, session_subcalls: 3 };
2304 let address = Address::from([0x42; 20]);
2305 let title = super::op_list_title(&address, 0x2a, 123_456, 42, 7, Some(stats));
2306
2307 assert!(title.contains("pc: 0x2a (42)"));
2308 assert!(title.contains(&format!("address: {}", super::full_checksum_address(&address))));
2309 assert!(title.contains("gasLeft: 123456"));
2310 assert!(title.contains("sessionTraceGasUsed: 789012"));
2311 assert!(title.contains("sessionSubcalls: 3"));
2312 assert!(title.contains("callGasUsed: 42"));
2313 assert!(title.contains("gasRefund: 7"));
2314 }
2315
2316 #[test]
2317 fn op_list_title_omits_aggregate_stats_when_unavailable() {
2318 let title = super::op_list_title(&Address::from([0x42; 20]), 0x2a, 123_456, 42, 7, None);
2319
2320 assert!(!title.contains("sessionTraceGasUsed"));
2321 assert!(!title.contains("sessionSubcalls"));
2322 }
2323
2324 #[test]
2325 fn op_list_title_uses_full_checksum_address() {
2326 let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045");
2327 let title = super::op_list_title(&address, 0x2a, 123_456, 42, 7, None);
2328
2329 assert!(title.contains("address: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2330 assert!(!title.contains('…'));
2331 }
2332
2333 #[test]
2334 fn stack_item_line_includes_decimal_preview() {
2335 let line = super::stack_item_line(0, 2, &U256::from(42), None, false);
2336 let text = line_text(&line);
2337
2338 assert!(text.starts_with("00| "));
2339 assert!(text.ends_with("2a | 42\n"));
2340 }
2341
2342 #[test]
2343 fn stack_item_line_keeps_stack_labels_after_decimal_preview() {
2344 let param = OpcodeParam { name: "offset", index: 0 };
2345 let line = super::stack_item_line(0, 2, &U256::from(16), Some(¶m), true);
2346
2347 assert!(line_text(&line).ends_with("10 | 16 | offset\n"));
2348 }
2349
2350 #[test]
2351 fn stack_item_line_highlights_decimal_preview_for_opcode_params() {
2352 let param = OpcodeParam { name: "offset", index: 0 };
2353 let line = super::stack_item_line(0, 2, &U256::from(16), Some(¶m), false);
2354 let decimal = line.spans.iter().find(|span| span.content.as_ref() == "16").unwrap();
2355
2356 assert_eq!(decimal.style, Style::new().fg(Color::Cyan));
2357 }
2358
2359 #[test]
2360 fn decimal_digits() {
2361 assert_eq!(super::decimal_digits(0), 1);
2362 assert_eq!(super::decimal_digits(1), 1);
2363 assert_eq!(super::decimal_digits(2), 1);
2364 assert_eq!(super::decimal_digits(9), 1);
2365 assert_eq!(super::decimal_digits(10), 2);
2366 assert_eq!(super::decimal_digits(11), 2);
2367 assert_eq!(super::decimal_digits(50), 2);
2368 assert_eq!(super::decimal_digits(99), 2);
2369 assert_eq!(super::decimal_digits(100), 3);
2370 assert_eq!(super::decimal_digits(101), 3);
2371 assert_eq!(super::decimal_digits(201), 3);
2372 assert_eq!(super::decimal_digits(999), 3);
2373 assert_eq!(super::decimal_digits(1000), 4);
2374 assert_eq!(super::decimal_digits(1001), 4);
2375 }
2376
2377 #[test]
2378 fn hex_digits() {
2379 assert_eq!(super::hex_digits(0), 1);
2380 assert_eq!(super::hex_digits(1), 1);
2381 assert_eq!(super::hex_digits(2), 1);
2382 assert_eq!(super::hex_digits(9), 1);
2383 assert_eq!(super::hex_digits(10), 1);
2384 assert_eq!(super::hex_digits(11), 1);
2385 assert_eq!(super::hex_digits(15), 1);
2386 assert_eq!(super::hex_digits(16), 2);
2387 assert_eq!(super::hex_digits(17), 2);
2388 assert_eq!(super::hex_digits(0xff), 2);
2389 assert_eq!(super::hex_digits(0x100), 3);
2390 assert_eq!(super::hex_digits(0x101), 3);
2391 }
2392}