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