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_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_step_notice_line(&self) -> Option<Line<'static>> {
621 self.current_step_notice_text().map(step_notice_line)
622 }
623
624 fn current_storage_access_line(&self) -> Option<Line<'static>> {
625 storage_access_at(self.debug_steps(), self.current_step).map(storage_access_line)
626 }
627
628 fn draw_data(&mut self, f: &mut Frame<'_>, area: Rect) {
629 if let Some(space) = self.active_storage() {
630 self.draw_storage(f, area, space);
631 } else {
632 self.draw_buffer(f, area);
633 }
634 }
635
636 fn draw_storage(&mut self, f: &mut Frame<'_>, area: Rect, space: StorageSpace) {
637 let accesses = self.storage_accesses(space);
638 let current_slot = storage_access_at(self.debug_steps(), self.current_step)
639 .filter(|access| access.space() == space)
640 .map(StorageAccess::slot);
641 self.draw_memory.current_storage_startline =
642 self.draw_memory.current_storage_startline.min(accesses.len().saturating_sub(1));
643
644 let index_width = decimal_digits(accesses.len()).max(2);
645 let mut lines = accesses
646 .values()
647 .copied()
648 .enumerate()
649 .skip(self.draw_memory.current_storage_startline)
650 .flat_map(|(index, access)| {
651 storage_slot_lines(index, index_width, access, current_slot == Some(access.slot()))
652 })
653 .collect::<Vec<_>>();
654 if lines.is_empty() {
655 lines.push(Line::from(Span::styled(
656 format!("No {} accessed in current call", space.noun()),
657 Style::new().add_modifier(Modifier::DIM),
658 )));
659 }
660
661 let count = accesses.len();
662 let suffix = if count == 1 { "slot" } else { "slots" };
663 let title = format!("{}: {count} accessed {suffix}", space.label());
664 let block = Block::default().title(title).borders(Borders::ALL);
665 let paragraph = Paragraph::new(lines).block(block).wrap(Wrap { trim: false });
666 f.render_widget(paragraph, area);
667 }
668
669 fn draw_buffer(&self, f: &mut Frame<'_>, area: Rect) {
670 let call = self.debug_call();
671 let step = self.current_step();
672 let buf: &[u8] = match self.active_buffer {
673 BufferKind::Memory => step.memory.as_ref().map_or(&[], |memory| memory.as_ref()),
674 BufferKind::Calldata => call.calldata.as_ref(),
675 BufferKind::Returndata => step.returndata.as_ref(),
676 };
677
678 let min_len = hex_digits(buf.len());
679
680 let mut offset = None;
682 let mut len = None;
683 let mut write_offset = None;
684 let mut write_size = None;
685 let mut color = None;
686 let stack_len = step.stack.as_ref().map_or(0, |s| s.len());
687 if stack_len > 0
688 && let Some(stack) = step.stack.as_ref()
689 && let Some(accesses) = get_buffer_accesses(step.op.get(), stack)
690 {
691 if let Some(read_access) = accesses.read {
692 offset = Some(read_access.1.offset);
693 len = Some(read_access.1.len);
694 color = Some(Color::Cyan);
695 }
696 if let Some(write_access) = accesses.write
697 && self.active_buffer == BufferKind::Memory
698 {
699 write_offset = Some(write_access.offset);
700 write_size = Some(write_access.len);
701 }
702 }
703
704 if self.current_step > 0 {
709 let prev_step = self.current_step - 1;
710 let prev_step = &self.debug_steps()[prev_step];
711 if let Some(stack) = prev_step.stack.as_ref()
712 && let Some(write_access) =
713 get_buffer_accesses(prev_step.op.get(), stack).and_then(|a| a.write)
714 && self.active_buffer == BufferKind::Memory
715 {
716 offset = Some(write_access.offset);
717 len = Some(write_access.len);
718 color = Some(Color::Green);
719 }
720 }
721
722 let height = area.height as usize;
723 let end_line = self.draw_memory.current_buf_startline + height;
724
725 let text: Vec<Line<'_>> = buf
726 .chunks(32)
727 .enumerate()
728 .skip(self.draw_memory.current_buf_startline)
729 .take_while(|(i, _)| *i < end_line)
730 .map(|(i, buf_word)| {
731 let mut spans = Vec::with_capacity(1 + 32 * 2 + 1 + 32 / 4 + 1);
732
733 spans.push(Span::styled(
735 format!("{:0min_len$x}| ", i * 32),
736 Style::new().fg(Color::White),
737 ));
738
739 hex_bytes_spans(buf_word, &mut spans, |j, _| {
741 let mut byte_color = Color::White;
742 let mut end = None;
743 let idx = i * 32 + j;
744 if let (Some(offset), Some(len), Some(color)) = (offset, len, color) {
745 end = Some(offset + len);
746 if (offset..offset + len).contains(&idx) {
747 byte_color = color;
751 }
752 }
753 if let (Some(write_offset), Some(write_size)) = (write_offset, write_size) {
754 let write_end = write_offset + write_size;
756 if let Some(read_end) = end {
757 let read_start = offset.unwrap();
758 if (write_offset..write_end).contains(&read_end) {
759 if (write_offset..read_end).contains(&idx) {
761 return Style::new().fg(Color::Yellow);
762 }
763 } else if (write_offset..write_end).contains(&read_start) {
764 if (read_start..write_end).contains(&idx) {
767 return Style::new().fg(Color::Yellow);
768 }
769 }
770 }
771 if (write_offset..write_end).contains(&idx) {
772 byte_color = Color::Red;
773 }
774 }
775
776 Style::new().fg(byte_color)
777 });
778
779 if self.buf_utf {
780 spans.push(Span::raw("|"));
781 for utf in buf_word.chunks(4) {
782 if let Ok(utf_str) = std::str::from_utf8(utf) {
783 spans.push(Span::raw(utf_str.replace('\0', ".")));
784 } else {
785 spans.push(Span::raw("."));
786 }
787 }
788 }
789
790 spans.push(Span::raw("\n"));
791
792 Line::from(spans)
793 })
794 .collect();
795
796 let title = self.active_buffer.title(buf.len());
797 let block = Block::default().title(title).borders(Borders::ALL);
798 let paragraph = Paragraph::new(text).block(block).wrap(Wrap { trim: true });
799 f.render_widget(paragraph, area);
800 }
801}
802
803#[derive(Clone, Debug, PartialEq, Eq)]
804struct ScopeVariable {
805 kind: ScopeVariableKind,
806 name: String,
807 value: Option<String>,
808}
809
810#[derive(Clone, Copy, Debug, PartialEq, Eq)]
811enum ScopeVariableKind {
812 Parameter,
813 Return,
814 Local,
815}
816
817struct ActiveInternalCall<'a> {
818 trace_node_idx: usize,
819 entry_step: usize,
820 end_step: usize,
821 decoded: &'a DecodedInternalCall,
822}
823
824const PRECOMPILE_NOTICE_PREFIX: &str = "precompile:";
825
826impl ScopeVariableKind {
827 const fn label(self) -> &'static str {
828 match self {
829 Self::Parameter => "param",
830 Self::Return => "return",
831 Self::Local => "local",
832 }
833 }
834
835 const fn color(self) -> Color {
836 match self {
837 Self::Parameter => Color::Cyan,
838 Self::Return => Color::Green,
839 Self::Local => Color::White,
840 }
841 }
842}
843
844impl TUIContext<'_> {
845 fn scope_variables(&mut self) -> Vec<ScopeVariable> {
846 let (scope, start) = {
847 let Ok((source_element, source)) = self.src_map() else {
848 return Vec::new();
849 };
850 let start = source_element.offset() as usize;
851 let end = start.saturating_add(source_element.length() as usize);
852 let Some(scope) = source.find_debug_scope(start, end) else {
853 return Vec::new();
854 };
855 (scope.clone(), start)
856 };
857
858 let parameter_values = self.decode_parameter_values(&scope);
859 let return_values = self.decode_return_values(&scope);
860 let mut variables = Vec::new();
861
862 variables.extend(scope.parameters.iter().enumerate().map(|(i, variable)| ScopeVariable {
863 kind: ScopeVariableKind::Parameter,
864 name: variable_name(variable, i, "arg"),
865 value: parameter_values.as_ref().and_then(|values| values.get(i).cloned()),
866 }));
867
868 variables.extend(scope.returns.iter().enumerate().map(|(i, variable)| ScopeVariable {
869 kind: ScopeVariableKind::Return,
870 name: variable_name(variable, i, "ret"),
871 value: return_values.as_ref().and_then(|values| values.get(i).cloned()),
872 }));
873
874 variables.extend(scope.visible_locals(start).enumerate().map(|(i, variable)| {
875 ScopeVariable {
876 kind: ScopeVariableKind::Local,
877 name: variable_name(variable, i, "local"),
878 value: None,
879 }
880 }));
881
882 variables
883 }
884
885 fn decode_parameter_values(&mut self, scope: &DebugSourceScope) -> Option<Vec<String>> {
886 let scope_signature = scope_function_signature(scope);
887 self.decode_internal_parameter_values(scope)
888 .or_else(|| decode_external_parameter_values(scope, &self.debug_call().calldata))
889 .or_else(|| {
890 self.debug_call().decoded.as_ref().and_then(|decoded| {
891 let call_data = decoded.call_data.as_ref()?;
892 scope_signature
893 .as_deref()
894 .is_some_and(|signature| signature == call_data.signature)
895 .then(|| call_data.args.clone())
896 })
897 })
898 }
899
900 fn decode_return_values(&mut self, scope: &DebugSourceScope) -> Option<Vec<String>> {
901 let current_step = self.absolute_current_step();
902 if let Some(values) = self
903 .active_internal_call()
904 .and_then(|active| {
905 (current_step >= active.end_step
906 && decoded_internal_name_matches(&active.decoded.func_name, scope))
907 .then(|| active.decoded.return_data.clone())
908 })
909 .flatten()
910 {
911 return Some(values);
912 }
913
914 if self.current_step + 1 < self.debug_steps().len()
915 || !matches!(
916 self.current_step().status,
917 Some(InstructionResult::Return | InstructionResult::Stop)
918 )
919 {
920 return None;
921 }
922
923 decode_external_return_values(
924 scope,
925 &self.debug_call().calldata,
926 &self.debug_call().returndata,
927 )
928 }
929
930 fn decode_internal_parameter_values(
931 &mut self,
932 scope: &DebugSourceScope,
933 ) -> Option<Vec<String>> {
934 let (args, trace_node_idx, entry_step) = {
935 let active = self.active_internal_call()?;
936 if !decoded_internal_name_matches(&active.decoded.func_name, scope) {
937 return None;
938 }
939
940 (active.decoded.args.clone(), active.trace_node_idx, active.entry_step)
941 };
942
943 if let Some(args) = args {
944 return Some(args);
945 }
946
947 let parameters = Parameters::parse(&scope.parameters_src).ok()?;
948 let node = self.debug_arena().iter().find(|node| {
949 node.trace_node_idx == trace_node_idx
950 && entry_step >= node.step_offset
951 && entry_step < node.step_offset.saturating_add(node.steps.len())
952 })?;
953 let step = node.steps.get(entry_step.checked_sub(node.step_offset)?)?;
954 decode_step_parameters(¶meters, step, Some(node.calldata.as_ref()))
955 }
956
957 fn active_internal_call(&mut self) -> Option<ActiveInternalCall<'_>> {
958 let current_node_idx = self.draw_memory.inner_call_index;
959 let trace_node_idx = self.debug_call().trace_node_idx;
960 let current_step = self.absolute_current_step();
961 let location = if let Some(cache) = self.draw_memory.active_internal_call
962 && cache.matches(current_node_idx, trace_node_idx, current_step)
963 {
964 cache.location
965 } else {
966 let location =
967 self.find_active_internal_call(current_node_idx, trace_node_idx, current_step);
968 self.draw_memory.active_internal_call = Some(ActiveInternalCallCache {
969 current_node_idx,
970 trace_node_idx,
971 absolute_step: current_step,
972 location,
973 });
974 location
975 }?;
976
977 self.active_internal_call_at(location)
978 }
979
980 fn find_active_internal_call(
981 &self,
982 current_node_idx: usize,
983 trace_node_idx: usize,
984 current_step: usize,
985 ) -> Option<ActiveInternalCallLocation> {
986 let mut active = None;
987
988 for (node_idx, node) in
989 self.debug_arena().iter().enumerate().take(current_node_idx.saturating_add(1))
990 {
991 if node.trace_node_idx != trace_node_idx {
992 continue;
993 }
994
995 for (step_idx, step) in node.steps.iter().enumerate() {
996 let marker_step = node.step_offset.saturating_add(step_idx);
997 if marker_step > current_step {
998 break;
999 }
1000
1001 let Some(decoded) = step.decoded.as_deref() else { continue };
1002 let DecodedTraceStep::InternalCall(_, end_step) = decoded else { continue };
1003 if current_step <= *end_step {
1004 active = Some(ActiveInternalCallLocation {
1005 trace_node_idx,
1006 marker_node_idx: node_idx,
1007 marker_step_idx: step_idx,
1008 entry_step: marker_step.saturating_add(1),
1009 end_step: *end_step,
1010 });
1011 }
1012 }
1013 }
1014
1015 active
1016 }
1017
1018 fn active_internal_call_at(
1019 &self,
1020 location: ActiveInternalCallLocation,
1021 ) -> Option<ActiveInternalCall<'_>> {
1022 let step = self
1023 .debug_arena()
1024 .get(location.marker_node_idx)?
1025 .steps
1026 .get(location.marker_step_idx)?;
1027 let DecodedTraceStep::InternalCall(decoded, end_step) = step.decoded.as_deref()? else {
1028 return None;
1029 };
1030 (*end_step == location.end_step).then_some(ActiveInternalCall {
1031 trace_node_idx: location.trace_node_idx,
1032 entry_step: location.entry_step,
1033 end_step: location.end_step,
1034 decoded,
1035 })
1036 }
1037
1038 fn absolute_current_step(&self) -> usize {
1039 self.debug_call().step_offset.saturating_add(self.current_step)
1040 }
1041}
1042
1043fn source_pane_title(
1044 call_kind_text: &str,
1045 source_name: Option<&str>,
1046 step_notice: Option<&str>,
1047) -> String {
1048 let mut title = call_kind_text.to_string();
1049 if let Some(step_notice) = step_notice {
1050 write!(title, " | {step_notice}").unwrap();
1051 }
1052 if let Some(source_name) = source_name {
1053 write!(title, " | {source_name}").unwrap();
1054 }
1055 title.push(' ');
1056 title
1057}
1058
1059fn variables_title(
1060 total_variables: usize,
1061 known_variables: usize,
1062 has_storage_access: bool,
1063 has_step_notice: bool,
1064) -> String {
1065 if total_variables == 0 && !has_storage_access && !has_step_notice {
1066 return "Variables".to_string();
1067 }
1068
1069 let mut title = format!("Variables: {known_variables}/{total_variables}");
1070 if has_step_notice {
1071 title.push_str(" | Trace");
1072 }
1073 if has_storage_access {
1074 title.push_str(" | Storage");
1075 }
1076 title
1077}
1078
1079fn shortcut_lines() -> Vec<Line<'static>> {
1080 let dimmed = Style::new().add_modifier(Modifier::DIM);
1081 vec![
1082 Line::from(Span::styled(
1083 "[q] quit | [j/k] op | [a/s] jump | [c/C] call | [g/G] start/end | [p] PC | [o] offset",
1084 dimmed,
1085 )),
1086 Line::from(Span::styled(
1087 "[/] search | [:] command | [n/N] repeat | [l] layout | [b] buffer",
1088 dimmed,
1089 )),
1090 Line::from(Span::styled(
1091 "[t] labels | [m] decode | [h] help | [J/K] stack scroll | [ctrl+j/k] data scroll | ['<char>] breakpoint",
1092 dimmed,
1093 )),
1094 ]
1095}
1096
1097const COMMAND_PROMPT_HINT: &str = " Enter: run | Esc: cancel | help: command list";
1098
1099fn command_prompt_line(input: &str, width: u16) -> Line<'static> {
1100 let width = width as usize;
1101 let fixed_width = 2;
1102 let hint_width = text_width(COMMAND_PROMPT_HINT);
1103 let input_width = text_width(input);
1104 let include_hint = fixed_width + input_width + hint_width <= width;
1105 let input_width = if include_hint {
1106 width.saturating_sub(fixed_width + hint_width)
1107 } else {
1108 width.saturating_sub(fixed_width)
1109 };
1110
1111 let mut spans = vec![
1112 Span::styled(":", Style::new().fg(Color::Cyan).add_modifier(Modifier::BOLD)),
1113 Span::raw(input_tail(input, input_width)),
1114 Span::styled("█", Style::new().fg(Color::Cyan)),
1115 ];
1116 if include_hint {
1117 spans.push(Span::styled(COMMAND_PROMPT_HINT, Style::new().add_modifier(Modifier::DIM)));
1118 }
1119 Line::from(spans)
1120}
1121
1122fn text_width(text: &str) -> usize {
1124 Span::raw(text).width()
1125}
1126
1127fn input_tail(input: &str, max_width: usize) -> String {
1128 if text_width(input) <= max_width {
1129 return input.to_string();
1130 }
1131 if max_width == 0 {
1132 return String::new();
1133 }
1134 if max_width == 1 {
1135 return "<".to_string();
1136 }
1137
1138 let tail_width = max_width - 1;
1141 let mut start = input.len();
1142 for (idx, _) in input.char_indices().rev() {
1143 if text_width(&input[idx..]) > tail_width {
1144 break;
1145 }
1146 start = idx;
1147 }
1148 format!("<{}", &input[start..])
1149}
1150
1151fn step_notice_title(line: &str) -> &'static str {
1152 if line.starts_with(PRECOMPILE_NOTICE_PREFIX) { "precompile call" } else { "decoded step" }
1153}
1154
1155fn step_notice_line(line: &str) -> Line<'static> {
1156 Line::from(Span::styled(line.to_string(), Style::new().fg(Color::Magenta)))
1157}
1158
1159fn scope_variable_line(variable: ScopeVariable) -> Line<'static> {
1160 let color = variable.kind.color();
1161 let mut spans = Vec::with_capacity(6);
1162 spans.push(Span::styled(variable.kind.label(), Style::new().fg(Color::Gray)));
1163 spans.push(Span::raw(" "));
1164 spans.push(Span::styled(variable.name, Style::new().fg(color).add_modifier(Modifier::BOLD)));
1165 spans.push(Span::raw(" = "));
1166 if let Some(value) = variable.value {
1167 spans.push(Span::styled(value, Style::new().fg(color)));
1168 } else {
1169 spans.push(Span::styled("<unavailable>", Style::new().fg(Color::Gray)));
1170 }
1171 Line::from(spans)
1172}
1173
1174fn storage_access_line(access: StorageAccess) -> Line<'static> {
1175 Line::from(Span::styled(access.describe(), Style::new().fg(Color::Yellow)))
1176}
1177
1178fn storage_slot_lines(
1179 index: usize,
1180 index_width: usize,
1181 access: StorageAccess,
1182 current: bool,
1183) -> [Line<'static>; 2] {
1184 let value_style = if current {
1185 Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD)
1186 } else {
1187 Style::new().fg(Color::White)
1188 };
1189 let prefix_width = index_width + 2;
1190 [
1191 Line::from(vec![
1192 Span::styled(format!("{index:0index_width$}| "), Style::new().fg(Color::Gray)),
1193 Span::styled(access.op(), value_style),
1194 Span::raw(" slot "),
1195 Span::styled(hex_u256(access.slot()), value_style),
1196 ]),
1197 Line::from(vec![
1198 Span::raw(" ".repeat(prefix_width)),
1199 Span::raw("value "),
1200 Span::styled(hex_u256(access.value()), value_style),
1201 ]),
1202 ]
1203}
1204
1205fn variable_name(variable: &DebugVariable, index: usize, fallback_prefix: &str) -> String {
1206 variable
1207 .name
1208 .as_deref()
1209 .filter(|name| !name.is_empty())
1210 .map(ToOwned::to_owned)
1211 .unwrap_or_else(|| format!("{fallback_prefix}{index}"))
1212}
1213
1214fn decoded_internal_name_matches(decoded_name: &str, scope: &DebugSourceScope) -> bool {
1215 if let Some((contract_name, function_name)) = decoded_name.rsplit_once("::") {
1216 return contract_name == scope.contract_name
1217 && decoded_function_matches(function_name, scope);
1218 }
1219 decoded_function_matches(decoded_name, scope)
1220}
1221
1222fn decoded_function_matches(decoded_name: &str, scope: &DebugSourceScope) -> bool {
1223 if decoded_name == scope.function_name {
1224 return true;
1225 }
1226 scope_function_signature(scope).as_deref().is_some_and(|signature| decoded_name == signature)
1227}
1228
1229fn decode_external_parameter_values(
1230 scope: &DebugSourceScope,
1231 calldata: &[u8],
1232) -> Option<Vec<String>> {
1233 let types = external_scope_parameter_types(scope, calldata)?;
1234
1235 decode_abi_sequence(&types, &calldata[4..])
1236}
1237
1238fn decode_external_return_values(
1239 scope: &DebugSourceScope,
1240 calldata: &[u8],
1241 returndata: &[u8],
1242) -> Option<Vec<String>> {
1243 external_scope_parameter_types(scope, calldata)?;
1244 let returns_src = scope.returns_src.as_deref()?;
1245 let returns = Parameters::parse(returns_src).ok()?;
1246 let types = resolved_types(&returns)?;
1247 decode_abi_sequence(&types, returndata)
1248}
1249
1250fn external_scope_parameter_types(
1251 scope: &DebugSourceScope,
1252 calldata: &[u8],
1253) -> Option<Vec<DynSolType>> {
1254 if calldata.len() < 4 {
1255 return None;
1256 }
1257
1258 let parameters = Parameters::parse(&scope.parameters_src).ok()?;
1259 let types = resolved_types(¶meters)?;
1260 let selector = function_selector(&scope.function_name, &types);
1261 if calldata.get(..4)? != selector.as_slice() {
1262 return None;
1263 }
1264
1265 Some(types)
1266}
1267
1268fn resolved_types(parameters: &Parameters<'_>) -> Option<Vec<DynSolType>> {
1269 parameters.params.iter().map(|param| param.resolve().ok()).collect()
1270}
1271
1272fn scope_function_signature(scope: &DebugSourceScope) -> Option<String> {
1273 let parameters = Parameters::parse(&scope.parameters_src).ok()?;
1274 let types = resolved_types(¶meters)?;
1275 Some(function_signature(&scope.function_name, &types))
1276}
1277
1278fn function_selector(function_name: &str, types: &[DynSolType]) -> [u8; 4] {
1279 let signature = function_signature(function_name, types);
1280 keccak256(signature.as_bytes())[..4].try_into().unwrap()
1281}
1282
1283fn decode_abi_sequence(types: &[DynSolType], data: &[u8]) -> Option<Vec<String>> {
1284 if types.is_empty() {
1285 return Some(Vec::new());
1286 }
1287
1288 let value = DynSolType::Tuple(types.to_vec()).abi_decode_sequence(data).ok()?;
1289 let values = value.as_fixed_seq()?;
1290 Some(values.iter().map(format_token).collect())
1291}
1292
1293fn op_list_title(
1294 address: &Address,
1295 pc: usize,
1296 gas_remaining: u64,
1297 call_gas_used: u64,
1298 gas_refund_counter: u64,
1299 stats: Option<DebuggerStats>,
1300) -> String {
1301 let address = full_checksum_address(address);
1302 let mut title = format!(
1303 "address: {address} | pc: 0x{pc:x} ({pc}) | gasLeft: {gas_remaining} | \
1304 callGasUsed: {call_gas_used} | gasRefund: {gas_refund_counter}"
1305 );
1306
1307 if let Some(stats) = stats {
1308 write!(
1309 title,
1310 " | sessionTraceGasUsed: {} | sessionSubcalls: {}",
1311 stats.session_trace_gas_used, stats.session_subcalls
1312 )
1313 .unwrap();
1314 }
1315
1316 title
1317}
1318
1319fn full_checksum_address(address: &Address) -> String {
1320 address.to_string()
1321}
1322
1323fn stack_item_line(
1324 i: usize,
1325 min_len: usize,
1326 stack_item: &U256,
1327 param: Option<&OpcodeParam>,
1328 stack_labels: bool,
1329) -> Line<'static> {
1330 let value_style =
1331 if param.is_some() { Style::new().fg(Color::Cyan) } else { Style::new().fg(Color::White) };
1332 let mut spans = Vec::with_capacity(1 + 32 * 2 + 5);
1333
1334 spans.push(Span::styled(format!("{i:0min_len$}| "), Style::new().fg(Color::White)));
1336
1337 hex_bytes_spans(&stack_item.to_be_bytes::<32>(), &mut spans, |_, _| value_style);
1339
1340 spans.push(Span::raw(" | "));
1341 spans.push(Span::styled(stack_item.to_string(), value_style));
1342
1343 if stack_labels && let Some(param) = param {
1344 spans.push(Span::raw(" | "));
1345 spans.push(Span::raw(param.name));
1346 }
1347
1348 spans.push(Span::raw("\n"));
1349
1350 Line::from(spans)
1351}
1352
1353struct SourceLines<'a> {
1355 lines: Vec<Line<'a>>,
1356 start_line: usize,
1357 max_line_num: usize,
1358}
1359
1360impl<'a> SourceLines<'a> {
1361 fn new(start_line: usize, end_line: usize) -> Self {
1362 Self { lines: Vec::new(), start_line, max_line_num: decimal_digits(end_line) }
1363 }
1364
1365 fn push(&mut self, line_number_style: Style, line: &'a str, line_style: Style) {
1366 self.push_raw(line_number_style, &[Span::styled(line, line_style)]);
1367 }
1368
1369 fn push_raw(&mut self, line_number_style: Style, spans: &[Span<'a>]) {
1370 let mut line_spans = Vec::with_capacity(4);
1371
1372 let line_number = format!(
1373 "{number: >width$} ",
1374 number = self.start_line + self.lines.len() + 1,
1375 width = self.max_line_num
1376 );
1377 line_spans.push(Span::styled(line_number, line_number_style));
1378
1379 line_spans.push(Span::raw(" "));
1381
1382 line_spans.extend_from_slice(spans);
1383
1384 self.lines.push(Line::from(line_spans));
1385 }
1386}
1387
1388fn hex_bytes_spans(bytes: &[u8], spans: &mut Vec<Span<'_>>, f: impl Fn(usize, u8) -> Style) {
1389 for (i, &byte) in bytes.iter().enumerate() {
1390 if i > 0 {
1391 spans.push(Span::raw(" "));
1392 }
1393 spans.push(Span::styled(alloy_primitives::hex::encode([byte]), f(i, byte)));
1394 }
1395}
1396
1397fn decimal_digits(n: usize) -> usize {
1401 n.checked_ilog10().unwrap_or(0) as usize + 1
1402}
1403
1404fn hex_digits(n: usize) -> usize {
1408 n.checked_ilog(16).unwrap_or(0) as usize + 1
1409}
1410
1411#[cfg(test)]
1412mod tests {
1413 use super::TUIContext;
1414 use crate::{
1415 DebugNode, DebuggerLayout,
1416 debugger::{DebuggerContext, DebuggerStats},
1417 op::OpcodeParam,
1418 };
1419 use alloy_dyn_abi::parser::Parameters;
1420 use alloy_primitives::{Address, Bytes, U256, address};
1421 use foundry_evm_core::Breakpoints;
1422 use foundry_evm_traces::debug::{ContractSources, DebugSourceScope, DebugVariable};
1423 use ratatui::{
1424 Terminal,
1425 backend::TestBackend,
1426 layout::Rect,
1427 style::{Color, Style},
1428 text::Line,
1429 };
1430 use revm::{bytecode::opcode::OpCode, interpreter::InstructionResult};
1431 use revm_inspectors::tracing::types::{
1432 CallKind, CallTraceStep, DecodedCallData, DecodedCallTrace, DecodedInternalCall,
1433 DecodedTraceStep, StorageChange, StorageChangeReason,
1434 };
1435
1436 fn line_text(line: &Line<'_>) -> String {
1437 line.spans.iter().map(|span| span.content.as_ref()).collect()
1438 }
1439
1440 fn scope(function_name: &str, parameters_src: &str) -> DebugSourceScope {
1441 DebugSourceScope {
1442 contract_name: "DebugMe".to_string(),
1443 function_name: function_name.to_string(),
1444 range: 0..100,
1445 body_range: 10..90,
1446 parameters_src: parameters_src.to_string(),
1447 returns_src: None,
1448 parameters: Vec::new(),
1449 returns: Vec::new(),
1450 locals: Vec::new(),
1451 }
1452 }
1453
1454 fn scope_with_returns(
1455 function_name: &str,
1456 parameters_src: &str,
1457 returns_src: &str,
1458 ) -> DebugSourceScope {
1459 DebugSourceScope {
1460 returns_src: Some(returns_src.to_string()),
1461 ..scope(function_name, parameters_src)
1462 }
1463 }
1464
1465 fn trace_step(stack: Vec<U256>) -> CallTraceStep {
1466 CallTraceStep {
1467 pc: 0,
1468 op: OpCode::STOP,
1469 stack: Some(stack.into_boxed_slice()),
1470 push_stack: None,
1471 memory: None,
1472 returndata: Bytes::new(),
1473 gas_remaining: 0,
1474 gas_refund_counter: 0,
1475 gas_used: 0,
1476 gas_cost: 0,
1477 storage_change: None,
1478 status: Some(InstructionResult::Stop),
1479 immediate_bytes: None,
1480 decoded: None,
1481 }
1482 }
1483
1484 fn internal_call_step(end_step: usize, return_data: Vec<String>) -> CallTraceStep {
1485 internal_call_step_named("DebugMe::foo", end_step, Some(Vec::new()), Some(return_data))
1486 }
1487
1488 fn internal_call_step_named(
1489 func_name: &str,
1490 end_step: usize,
1491 args: Option<Vec<String>>,
1492 return_data: Option<Vec<String>>,
1493 ) -> CallTraceStep {
1494 let mut step = trace_step(Vec::new());
1495 step.decoded = Some(Box::new(DecodedTraceStep::InternalCall(
1496 DecodedInternalCall { func_name: func_name.to_string(), args, return_data },
1497 end_step,
1498 )));
1499 step
1500 }
1501
1502 fn internal_call_step_without_args(end_step: usize) -> CallTraceStep {
1503 let mut step = trace_step(Vec::new());
1504 step.decoded = Some(Box::new(DecodedTraceStep::InternalCall(
1505 DecodedInternalCall {
1506 func_name: "DebugMe::foo".to_string(),
1507 args: None,
1508 return_data: None,
1509 },
1510 end_step,
1511 )));
1512 step
1513 }
1514
1515 fn debug_node(
1516 trace_node_idx: usize,
1517 step_offset: usize,
1518 steps: Vec<CallTraceStep>,
1519 ) -> DebugNode {
1520 let mut node = DebugNode::new(Address::ZERO, CallKind::Call, steps, Bytes::new(), 0, None);
1521 node.trace_node_idx = trace_node_idx;
1522 node.step_offset = step_offset;
1523 node
1524 }
1525
1526 fn context_with_arena(arena: Vec<DebugNode>) -> DebuggerContext {
1527 DebuggerContext {
1528 debug_arena: arena,
1529 stats: None,
1530 identified_contracts: Default::default(),
1531 contracts_sources: ContractSources::default(),
1532 breakpoints: Breakpoints::default(),
1533 layout: Default::default(),
1534 }
1535 }
1536
1537 fn abi_word(value: U256) -> [u8; 32] {
1538 value.to_be_bytes::<32>()
1539 }
1540
1541 #[test]
1542 fn draw_buffer_handles_missing_memory_snapshot() {
1543 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1544 let tui = TUIContext::new(&mut context);
1545 let backend = TestBackend::new(80, 4);
1546 let mut terminal = Terminal::new(backend).unwrap();
1547
1548 terminal.draw(|f| tui.draw_buffer(f, Rect::new(0, 0, 80, 4))).unwrap();
1549
1550 let screen = terminal
1551 .backend()
1552 .buffer()
1553 .content()
1554 .iter()
1555 .map(|cell| cell.symbol())
1556 .collect::<String>();
1557 assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1558 }
1559
1560 #[test]
1561 fn storage_explorer_draws_accessed_slots() {
1562 let mut first = trace_step(Vec::new());
1563 first.storage_change = Some(Box::new(StorageChange {
1564 key: U256::ZERO,
1565 value: U256::from(42),
1566 had_value: None,
1567 reason: StorageChangeReason::SSTORE,
1568 }));
1569 let mut second = trace_step(Vec::new());
1570 second.storage_change = Some(Box::new(StorageChange {
1571 key: U256::from(1),
1572 value: U256::from(0xbeef),
1573 had_value: None,
1574 reason: StorageChangeReason::SSTORE,
1575 }));
1576 let mut latest = trace_step(Vec::new());
1577 latest.storage_change = Some(Box::new(StorageChange {
1578 key: U256::ZERO,
1579 value: U256::from(43),
1580 had_value: Some(U256::from(42)),
1581 reason: StorageChangeReason::SSTORE,
1582 }));
1583 let mut context = context_with_arena(vec![debug_node(0, 0, vec![first, second, latest])]);
1584 let mut tui = TUIContext::new(&mut context);
1585 tui.current_step = 2;
1586 let backend = TestBackend::new(100, 6);
1587 let mut terminal = Terminal::new(backend).unwrap();
1588
1589 terminal
1590 .draw(|f| tui.draw_storage(f, Rect::new(0, 0, 100, 6), super::StorageSpace::Persistent))
1591 .unwrap();
1592
1593 let screen = terminal
1594 .backend()
1595 .buffer()
1596 .content()
1597 .iter()
1598 .map(|cell| cell.symbol())
1599 .collect::<String>();
1600 assert!(screen.contains("Storage: 2 accessed slots"));
1601 assert!(screen.contains("SSTORE slot 0x0"));
1602 assert!(screen.contains("value 0x2b"));
1603 assert!(screen.contains("SSTORE slot 0x1"));
1604 assert!(screen.contains("value 0xbeef"));
1605 }
1606
1607 #[test]
1608 fn hidden_source_pane_omits_source_panel() {
1609 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1610 context.layout = DebuggerLayout::Horizontal;
1611 let mut tui = TUIContext::new(&mut context);
1612 tui.init();
1613 tui.show_source = false;
1614 let backend = TestBackend::new(220, 30);
1615 let mut terminal = Terminal::new(backend).unwrap();
1616
1617 terminal.draw(|f| tui.draw_layout(f)).unwrap();
1618
1619 let screen = terminal
1620 .backend()
1621 .buffer()
1622 .content()
1623 .iter()
1624 .map(|cell| cell.symbol())
1625 .collect::<String>();
1626 assert!(!screen.contains("Contract call"));
1627 assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1628 }
1629
1630 #[test]
1631 fn hidden_opcodes_pane_omits_opcode_panel() {
1632 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1633 context.layout = DebuggerLayout::Horizontal;
1634 let mut tui = TUIContext::new(&mut context);
1635 tui.init();
1636 tui.show_opcodes = false;
1637 let backend = TestBackend::new(220, 30);
1638 let mut terminal = Terminal::new(backend).unwrap();
1639
1640 terminal.draw(|f| tui.draw_layout(f)).unwrap();
1641
1642 let screen = terminal
1643 .backend()
1644 .buffer()
1645 .content()
1646 .iter()
1647 .map(|cell| cell.symbol())
1648 .collect::<String>();
1649 assert!(!screen.contains("address:"));
1650 assert!(screen.contains("Contract call"));
1651 assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1652 }
1653
1654 #[test]
1655 fn hidden_variables_pane_omits_variables_panel() {
1656 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1657 context.layout = DebuggerLayout::Horizontal;
1658 let mut tui = TUIContext::new(&mut context);
1659 tui.init();
1660 tui.show_variables = false;
1661 let backend = TestBackend::new(220, 30);
1662 let mut terminal = Terminal::new(backend).unwrap();
1663
1664 terminal.draw(|f| tui.draw_layout(f)).unwrap();
1665
1666 let screen = terminal
1667 .backend()
1668 .buffer()
1669 .content()
1670 .iter()
1671 .map(|cell| cell.symbol())
1672 .collect::<String>();
1673 assert!(!screen.contains("Variables"));
1674 assert!(screen.contains("Stack"));
1675 assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1676 }
1677
1678 #[test]
1679 fn hidden_stack_pane_omits_stack_panel() {
1680 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1681 context.layout = DebuggerLayout::Horizontal;
1682 let mut tui = TUIContext::new(&mut context);
1683 tui.init();
1684 tui.show_stack = false;
1685 let backend = TestBackend::new(220, 30);
1686 let mut terminal = Terminal::new(backend).unwrap();
1687
1688 terminal.draw(|f| tui.draw_layout(f)).unwrap();
1689
1690 let screen = terminal
1691 .backend()
1692 .buffer()
1693 .content()
1694 .iter()
1695 .map(|cell| cell.symbol())
1696 .collect::<String>();
1697 assert!(!screen.contains("Stack:"));
1698 assert!(screen.contains("Variables"));
1699 assert!(screen.contains("Memory (max expansion: 0 bytes)"));
1700 }
1701
1702 #[test]
1703 fn hidden_data_pane_omits_data_panel() {
1704 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1705 context.layout = DebuggerLayout::Horizontal;
1706 let mut tui = TUIContext::new(&mut context);
1707 tui.init();
1708 tui.show_opcodes = false;
1709 tui.show_variables = false;
1710 tui.show_stack = false;
1711 tui.show_data = false;
1712 let backend = TestBackend::new(80, 30);
1713 let mut terminal = Terminal::new(backend).unwrap();
1714
1715 terminal.draw(|f| tui.draw_layout(f)).unwrap();
1716
1717 let screen = terminal
1718 .backend()
1719 .buffer()
1720 .content()
1721 .iter()
1722 .map(|cell| cell.symbol())
1723 .collect::<String>();
1724 assert!(!screen.contains("Memory (max expansion: 0 bytes)"));
1725 assert!(screen.contains("Contract call"));
1726 }
1727
1728 #[test]
1729 fn command_prompt_draws_in_bordered_block() {
1730 let mut context = context_with_arena(vec![debug_node(0, 0, vec![trace_step(Vec::new())])]);
1731 let mut tui = TUIContext::new(&mut context);
1732 tui.command_input = Some("pc 0".to_string());
1733 let backend = TestBackend::new(120, 6);
1734 let mut terminal = Terminal::new(backend).unwrap();
1735
1736 terminal.draw(|f| tui.draw_footer(f, Rect::new(0, 0, 120, 6))).unwrap();
1737
1738 let screen = terminal
1739 .backend()
1740 .buffer()
1741 .content()
1742 .iter()
1743 .map(|cell| cell.symbol())
1744 .collect::<String>();
1745 assert!(screen.contains("Command"));
1746 assert!(screen.contains(":pc 0"));
1747 assert!(screen.contains("Enter: run"));
1748 assert!(screen.contains("[:] command"));
1749 }
1750
1751 #[test]
1752 fn command_prompt_line_clips_long_input_to_tail() {
1753 let input = "continue 0123456789abcdef";
1754 let line = super::command_prompt_line(input, 12);
1755 let text = line_text(&line);
1756
1757 assert!(line.width() <= 12);
1758 assert_eq!(text, ":<789abcdef█");
1759 assert!(!text.contains("Enter: run"));
1760 }
1761
1762 #[test]
1763 fn command_prompt_line_clips_wide_unicode_to_width() {
1764 let input = "界界界界界界界界";
1767 let line = super::command_prompt_line(input, 8);
1768
1769 assert!(line.width() <= 8);
1770 }
1771
1772 #[test]
1773 fn decode_external_parameter_values_decodes_named_params() {
1774 let scope = scope("foo", "(uint256 amount, bool ok)");
1775 let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1776 let types = super::resolved_types(¶meters).unwrap();
1777 let mut calldata = Vec::new();
1778 calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1779 calldata.extend_from_slice(&abi_word(U256::from(42)));
1780 calldata.extend_from_slice(&abi_word(U256::from(1)));
1781
1782 let values = super::decode_external_parameter_values(&scope, &calldata).unwrap();
1783
1784 assert_eq!(values, ["42", "true"]);
1785 }
1786
1787 #[test]
1788 fn decode_external_parameter_values_rejects_selector_mismatch() {
1789 let scope = scope("foo", "(uint256 amount)");
1790 let parameters = Parameters::parse("(uint256 amount)").unwrap();
1791 let types = super::resolved_types(¶meters).unwrap();
1792 let mut calldata = Vec::new();
1793 calldata.extend_from_slice(&super::function_selector("bar", &types));
1794 calldata.extend_from_slice(&abi_word(U256::from(42)));
1795
1796 assert_eq!(super::decode_external_parameter_values(&scope, &calldata), None);
1797 }
1798
1799 #[test]
1800 fn decode_external_return_values_decodes_named_returns() {
1801 let scope = scope_with_returns("foo", "(uint256 amount)", "(uint256 total, bool ok)");
1802 let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1803 let types = super::resolved_types(¶meters).unwrap();
1804 let mut calldata = Vec::new();
1805 calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1806 calldata.extend_from_slice(&abi_word(U256::from(42)));
1807 let mut returndata = Vec::new();
1808 returndata.extend_from_slice(&abi_word(U256::from(99)));
1809 returndata.extend_from_slice(&abi_word(U256::from(1)));
1810
1811 let values = super::decode_external_return_values(&scope, &calldata, &returndata).unwrap();
1812
1813 assert_eq!(values, ["99", "true"]);
1814 }
1815
1816 #[test]
1817 fn decode_return_values_uses_external_output_at_frame_end() {
1818 let scope = scope_with_returns("foo", "()", "(uint256 total)");
1819 let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1820 let types = super::resolved_types(¶meters).unwrap();
1821 let mut calldata = Vec::new();
1822 calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1823 let mut node = debug_node(
1824 0,
1825 0,
1826 vec![trace_step(Vec::new()), {
1827 let mut step = trace_step(Vec::new());
1828 step.op = OpCode::RETURN;
1829 step.status = Some(InstructionResult::Return);
1830 step
1831 }],
1832 );
1833 node.calldata = Bytes::from(calldata);
1834 node.returndata = Bytes::from(abi_word(U256::from(123)).to_vec());
1835 let mut context = context_with_arena(vec![node]);
1836 let mut tui = TUIContext::new(&mut context);
1837 tui.current_step = 1;
1838
1839 assert_eq!(tui.decode_return_values(&scope), Some(vec!["123".to_string()]));
1840 }
1841
1842 #[test]
1843 fn decode_return_values_waits_for_external_frame_end() {
1844 let scope = scope_with_returns("foo", "()", "(uint256 total)");
1845 let parameters = Parameters::parse(&scope.parameters_src).unwrap();
1846 let types = super::resolved_types(¶meters).unwrap();
1847 let mut calldata = Vec::new();
1848 calldata.extend_from_slice(&super::function_selector(&scope.function_name, &types));
1849 let mut node = debug_node(0, 0, vec![trace_step(Vec::new()), trace_step(Vec::new())]);
1850 node.calldata = Bytes::from(calldata);
1851 node.returndata = Bytes::from(abi_word(U256::from(123)).to_vec());
1852 let mut context = context_with_arena(vec![node]);
1853 let mut tui = TUIContext::new(&mut context);
1854
1855 assert_eq!(tui.decode_return_values(&scope), None);
1856 }
1857
1858 #[test]
1859 fn scope_function_signature_includes_resolved_parameter_types() {
1860 let scope = scope("foo", "(uint256 amount, bool ok)");
1861
1862 assert_eq!(super::scope_function_signature(&scope).as_deref(), Some("foo(uint256,bool)"));
1863 }
1864
1865 #[test]
1866 fn decode_parameter_values_rejects_decoded_call_data_for_wrong_overload() {
1867 let mut node = debug_node(0, 0, vec![trace_step(Vec::new())]);
1868 node.decoded = Some(Box::new(DecodedCallTrace {
1869 call_data: Some(DecodedCallData {
1870 signature: "foo(address)".to_string(),
1871 args: vec!["0x000000000000000000000000000000000000002a".to_string()],
1872 }),
1873 ..Default::default()
1874 }));
1875 let mut context = context_with_arena(vec![node]);
1876 let mut tui = TUIContext::new(&mut context);
1877
1878 assert_eq!(tui.decode_parameter_values(&scope("foo", "(uint256 amount)")), None);
1879 }
1880
1881 #[test]
1882 fn decode_step_parameters_reads_static_values_from_stack() {
1883 let step = trace_step(vec![U256::from(42), U256::from(1)]);
1884 let parameters = Parameters::parse("(uint256 amount, bool ok)").unwrap();
1885 let values = super::decode_step_parameters(¶meters, &step, None).unwrap();
1886
1887 assert_eq!(values, ["42", "true"]);
1888 }
1889
1890 #[test]
1891 fn decode_internal_parameter_values_uses_absolute_entry_step() {
1892 let mut context = context_with_arena(vec![
1893 debug_node(0, 0, vec![internal_call_step_without_args(2)]),
1894 debug_node(1, 0, vec![trace_step(Vec::new())]),
1895 debug_node(0, 1, vec![trace_step(vec![U256::from(42)])]),
1896 ]);
1897 let mut tui = TUIContext::new(&mut context);
1898 tui.draw_memory.inner_call_index = 2;
1899
1900 assert_eq!(
1901 tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")),
1902 Some(vec!["42".to_string()])
1903 );
1904 }
1905
1906 #[test]
1907 fn decode_internal_parameter_values_passes_calldata_to_fallback_decoder() {
1908 let digest = U256::from(0x1234);
1909 let offset = 0x44;
1910 let mut calldata = vec![0; offset];
1911 calldata.extend_from_slice(&[0x11, 0x22, 0x33]);
1912
1913 let mut entry_node =
1914 debug_node(0, 1, vec![trace_step(vec![digest, U256::from(offset), U256::from(3)])]);
1915 entry_node.calldata = Bytes::from(calldata);
1916
1917 let mut context = context_with_arena(vec![
1918 debug_node(0, 0, vec![internal_call_step_without_args(2)]),
1919 debug_node(1, 0, vec![trace_step(Vec::new())]),
1920 entry_node,
1921 ]);
1922 let mut tui = TUIContext::new(&mut context);
1923 tui.draw_memory.inner_call_index = 2;
1924
1925 assert_eq!(
1926 tui.decode_internal_parameter_values(&scope(
1927 "foo",
1928 "(bytes32 digest, bytes calldata signature)"
1929 )),
1930 Some(vec![
1931 "0x0000000000000000000000000000000000000000000000000000000000001234".to_string(),
1932 "0x112233".to_string(),
1933 ])
1934 );
1935 }
1936
1937 #[test]
1938 fn decode_internal_parameter_values_accepts_matching_overload_args() {
1939 let mut context = context_with_arena(vec![debug_node(
1940 0,
1941 0,
1942 vec![internal_call_step_named(
1943 "DebugMe::foo(uint256)",
1944 2,
1945 Some(vec!["42".to_string()]),
1946 None,
1947 )],
1948 )]);
1949 let mut tui = TUIContext::new(&mut context);
1950
1951 assert_eq!(
1952 tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")),
1953 Some(vec!["42".to_string()])
1954 );
1955 }
1956
1957 #[test]
1958 fn decode_internal_parameter_values_rejects_wrong_overload_args() {
1959 let mut context = context_with_arena(vec![debug_node(
1960 0,
1961 0,
1962 vec![internal_call_step_named(
1963 "DebugMe::foo(address)",
1964 2,
1965 Some(vec!["0x000000000000000000000000000000000000002a".to_string()]),
1966 None,
1967 )],
1968 )]);
1969 let mut tui = TUIContext::new(&mut context);
1970
1971 assert_eq!(tui.decode_internal_parameter_values(&scope("foo", "(uint256 amount)")), None);
1972 }
1973
1974 #[test]
1975 fn decode_return_values_uses_absolute_internal_call_end_step() {
1976 let mut context = context_with_arena(vec![debug_node(
1977 0,
1978 3,
1979 vec![internal_call_step(4, vec!["99".to_string()]), trace_step(Vec::new())],
1980 )]);
1981 let mut tui = TUIContext::new(&mut context);
1982 tui.current_step = 1;
1983
1984 assert_eq!(tui.decode_return_values(&scope("foo", "()")), Some(vec!["99".to_string()]));
1985 }
1986
1987 #[test]
1988 fn decode_return_values_finds_internal_call_split_by_child_node() {
1989 let mut context = context_with_arena(vec![
1990 debug_node(0, 0, vec![internal_call_step(2, vec!["7".to_string()])]),
1991 debug_node(1, 0, vec![trace_step(Vec::new())]),
1992 debug_node(0, 2, vec![trace_step(Vec::new())]),
1993 ]);
1994 let mut tui = TUIContext::new(&mut context);
1995 tui.draw_memory.inner_call_index = 2;
1996
1997 assert_eq!(tui.decode_return_values(&scope("foo", "()")), Some(vec!["7".to_string()]));
1998 }
1999
2000 #[test]
2001 fn decode_return_values_rejects_wrong_overload() {
2002 let mut context = context_with_arena(vec![debug_node(
2003 0,
2004 0,
2005 vec![
2006 internal_call_step_named(
2007 "DebugMe::foo(address)",
2008 1,
2009 None,
2010 Some(vec!["99".to_string()]),
2011 ),
2012 trace_step(Vec::new()),
2013 ],
2014 )]);
2015 let mut tui = TUIContext::new(&mut context);
2016 tui.current_step = 1;
2017
2018 assert_eq!(tui.decode_return_values(&scope("foo", "(uint256 amount)")), None);
2019 }
2020
2021 #[test]
2022 fn active_internal_call_caches_by_current_node_and_step() {
2023 let mut context = context_with_arena(vec![debug_node(
2024 0,
2025 0,
2026 vec![internal_call_step(2, vec!["1".to_string()]), trace_step(Vec::new())],
2027 )]);
2028 let mut tui = TUIContext::new(&mut context);
2029
2030 assert!(tui.active_internal_call().is_some());
2031 let cache = tui.draw_memory.active_internal_call;
2032 assert!(cache.and_then(|cache| cache.location).is_some());
2033
2034 assert!(tui.active_internal_call().is_some());
2035 assert_eq!(tui.draw_memory.active_internal_call, cache);
2036
2037 tui.current_step = 1;
2038 assert!(tui.active_internal_call().is_some());
2039 assert_ne!(tui.draw_memory.active_internal_call, cache);
2040 }
2041
2042 #[test]
2043 fn decoded_internal_name_matches_exact_contract_and_function() {
2044 let scope = scope("foo", "()");
2045
2046 assert!(super::decoded_internal_name_matches("DebugMe::foo", &scope));
2047 assert!(!super::decoded_internal_name_matches("DebugMe::barfoo", &scope));
2048 assert!(!super::decoded_internal_name_matches("Other::foo", &scope));
2049 }
2050
2051 #[test]
2052 fn decoded_internal_name_matches_canonical_signature_for_overloads() {
2053 let scope = scope("foo", "(uint256 amount)");
2054
2055 assert!(super::decoded_internal_name_matches("DebugMe::foo(uint256)", &scope));
2056 assert!(!super::decoded_internal_name_matches("DebugMe::foo(address)", &scope));
2057 assert!(!super::decoded_internal_name_matches("Other::foo(uint256)", &scope));
2058 }
2059
2060 #[test]
2061 fn scope_variable_line_marks_unavailable_locals() {
2062 let variable = super::ScopeVariable {
2063 kind: super::ScopeVariableKind::Local,
2064 name: "sum".to_string(),
2065 value: None,
2066 };
2067
2068 assert_eq!(line_text(&super::scope_variable_line(variable)), "local sum = <unavailable>");
2069 }
2070
2071 #[test]
2072 fn storage_access_line_formats_sload() {
2073 let mut step = trace_step(Vec::new());
2074 step.storage_change = Some(Box::new(StorageChange {
2075 key: U256::from(1),
2076 value: U256::from(42),
2077 had_value: None,
2078 reason: StorageChangeReason::SLOAD,
2079 }));
2080 let steps = [step];
2081 let access = super::storage_access_at(&steps, 0).unwrap();
2082
2083 assert_eq!(line_text(&super::storage_access_line(access)), "storage SLOAD slot 0x1 = 0x2a");
2084 }
2085
2086 #[test]
2087 fn storage_access_line_formats_sstore_with_previous_value() {
2088 let mut step = trace_step(Vec::new());
2089 step.storage_change = Some(Box::new(StorageChange {
2090 key: U256::from(1),
2091 value: U256::from(42),
2092 had_value: Some(U256::from(7)),
2093 reason: StorageChangeReason::SSTORE,
2094 }));
2095 let steps = [step];
2096 let access = super::storage_access_at(&steps, 0).unwrap();
2097
2098 assert_eq!(
2099 line_text(&super::storage_access_line(access)),
2100 "storage SSTORE slot 0x1: 0x7 -> 0x2a"
2101 );
2102 }
2103
2104 #[test]
2105 fn current_storage_access_line_uses_next_stack_snapshot_for_warm_sload() {
2106 let mut step = trace_step(vec![U256::from(1)]);
2107 step.op = OpCode::SLOAD;
2108 step.storage_change = None;
2109 let next_step = trace_step(vec![U256::from(42)]);
2110 let mut context = context_with_arena(vec![debug_node(0, 0, vec![step, next_step])]);
2111 let tui = TUIContext::new(&mut context);
2112
2113 assert_eq!(
2114 line_text(&tui.current_storage_access_line().unwrap()),
2115 "storage SLOAD slot 0x1 = 0x2a"
2116 );
2117 }
2118
2119 #[test]
2120 fn current_storage_access_line_uses_next_stack_snapshot_for_tload() {
2121 let mut step = trace_step(vec![U256::from(1)]);
2122 step.op = OpCode::TLOAD;
2123 let next_step = trace_step(vec![U256::from(42)]);
2124 let mut context = context_with_arena(vec![debug_node(0, 0, vec![step, next_step])]);
2125 let tui = TUIContext::new(&mut context);
2126
2127 assert_eq!(
2128 line_text(&tui.current_storage_access_line().unwrap()),
2129 "transient storage TLOAD slot 0x1 = 0x2a"
2130 );
2131 }
2132
2133 #[test]
2134 fn variable_name_falls_back_for_unnamed_values() {
2135 let variable = DebugVariable { name: None, declaration: 0..1, scope: 0..2 };
2136
2137 assert_eq!(super::variable_name(&variable, 2, "arg"), "arg2");
2138 }
2139
2140 #[test]
2141 fn current_step_notice_text_reads_decoded_line_steps() {
2142 let mut step = trace_step(Vec::new());
2143 step.decoded = Some(Box::new(DecodedTraceStep::Line(
2144 "precompile: PRECOMPILES::sha256(0x68656c6c6f)".to_string(),
2145 )));
2146 let mut context = context_with_arena(vec![debug_node(0, 0, vec![step])]);
2147 let tui = TUIContext::new(&mut context);
2148
2149 assert_eq!(
2150 tui.current_step_notice_text(),
2151 Some("precompile: PRECOMPILES::sha256(0x68656c6c6f)")
2152 );
2153 }
2154
2155 #[test]
2156 fn source_pane_title_includes_precompile_notice_before_source() {
2157 assert_eq!(
2158 super::source_pane_title(
2159 "Contract call",
2160 Some("test/Precompile.t.sol"),
2161 Some("precompile call")
2162 ),
2163 "Contract call | precompile call | test/Precompile.t.sol "
2164 );
2165 }
2166
2167 #[test]
2168 fn variables_title_tracks_decoded_step_notices() {
2169 assert_eq!(super::variables_title(0, 0, false, false), "Variables");
2170 assert_eq!(super::variables_title(0, 0, false, true), "Variables: 0/0 | Trace");
2171 assert_eq!(super::variables_title(2, 1, true, true), "Variables: 1/2 | Trace | Storage");
2172 }
2173
2174 #[test]
2175 fn step_notice_line_highlights_precompile_clue() {
2176 let line = super::step_notice_line("precompile: PRECOMPILES::sha256(0x68656c6c6f)");
2177
2178 assert_eq!(line_text(&line), "precompile: PRECOMPILES::sha256(0x68656c6c6f)");
2179 assert_eq!(line.spans[0].style, Style::new().fg(Color::Magenta));
2180 assert_eq!(super::step_notice_title(&line_text(&line)), "precompile call");
2181 }
2182
2183 #[test]
2184 fn op_list_title_includes_gas_and_subcall_stats() {
2185 let stats = DebuggerStats { session_trace_gas_used: 789_012, session_subcalls: 3 };
2186 let address = Address::from([0x42; 20]);
2187 let title = super::op_list_title(&address, 0x2a, 123_456, 42, 7, Some(stats));
2188
2189 assert!(title.contains("pc: 0x2a (42)"));
2190 assert!(title.contains(&format!("address: {}", super::full_checksum_address(&address))));
2191 assert!(title.contains("gasLeft: 123456"));
2192 assert!(title.contains("sessionTraceGasUsed: 789012"));
2193 assert!(title.contains("sessionSubcalls: 3"));
2194 assert!(title.contains("callGasUsed: 42"));
2195 assert!(title.contains("gasRefund: 7"));
2196 }
2197
2198 #[test]
2199 fn op_list_title_omits_aggregate_stats_when_unavailable() {
2200 let title = super::op_list_title(&Address::from([0x42; 20]), 0x2a, 123_456, 42, 7, None);
2201
2202 assert!(!title.contains("sessionTraceGasUsed"));
2203 assert!(!title.contains("sessionSubcalls"));
2204 }
2205
2206 #[test]
2207 fn op_list_title_uses_full_checksum_address() {
2208 let address = address!("0xd8da6bf26964af9d7eed9e03e53415d37aa96045");
2209 let title = super::op_list_title(&address, 0x2a, 123_456, 42, 7, None);
2210
2211 assert!(title.contains("address: 0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045"));
2212 assert!(!title.contains('…'));
2213 }
2214
2215 #[test]
2216 fn stack_item_line_includes_decimal_preview() {
2217 let line = super::stack_item_line(0, 2, &U256::from(42), None, false);
2218 let text = line_text(&line);
2219
2220 assert!(text.starts_with("00| "));
2221 assert!(text.ends_with("2a | 42\n"));
2222 }
2223
2224 #[test]
2225 fn stack_item_line_keeps_stack_labels_after_decimal_preview() {
2226 let param = OpcodeParam { name: "offset", index: 0 };
2227 let line = super::stack_item_line(0, 2, &U256::from(16), Some(¶m), true);
2228
2229 assert!(line_text(&line).ends_with("10 | 16 | offset\n"));
2230 }
2231
2232 #[test]
2233 fn stack_item_line_highlights_decimal_preview_for_opcode_params() {
2234 let param = OpcodeParam { name: "offset", index: 0 };
2235 let line = super::stack_item_line(0, 2, &U256::from(16), Some(¶m), false);
2236 let decimal = line.spans.iter().find(|span| span.content.as_ref() == "16").unwrap();
2237
2238 assert_eq!(decimal.style, Style::new().fg(Color::Cyan));
2239 }
2240
2241 #[test]
2242 fn decimal_digits() {
2243 assert_eq!(super::decimal_digits(0), 1);
2244 assert_eq!(super::decimal_digits(1), 1);
2245 assert_eq!(super::decimal_digits(2), 1);
2246 assert_eq!(super::decimal_digits(9), 1);
2247 assert_eq!(super::decimal_digits(10), 2);
2248 assert_eq!(super::decimal_digits(11), 2);
2249 assert_eq!(super::decimal_digits(50), 2);
2250 assert_eq!(super::decimal_digits(99), 2);
2251 assert_eq!(super::decimal_digits(100), 3);
2252 assert_eq!(super::decimal_digits(101), 3);
2253 assert_eq!(super::decimal_digits(201), 3);
2254 assert_eq!(super::decimal_digits(999), 3);
2255 assert_eq!(super::decimal_digits(1000), 4);
2256 assert_eq!(super::decimal_digits(1001), 4);
2257 }
2258
2259 #[test]
2260 fn hex_digits() {
2261 assert_eq!(super::hex_digits(0), 1);
2262 assert_eq!(super::hex_digits(1), 1);
2263 assert_eq!(super::hex_digits(2), 1);
2264 assert_eq!(super::hex_digits(9), 1);
2265 assert_eq!(super::hex_digits(10), 1);
2266 assert_eq!(super::hex_digits(11), 1);
2267 assert_eq!(super::hex_digits(15), 1);
2268 assert_eq!(super::hex_digits(16), 2);
2269 assert_eq!(super::hex_digits(17), 2);
2270 assert_eq!(super::hex_digits(0xff), 2);
2271 assert_eq!(super::hex_digits(0x100), 3);
2272 assert_eq!(super::hex_digits(0x101), 3);
2273 }
2274}