forc_debug/cli/
commands.rs

1use crate::{
2    cli::state::{DebuggerHelper, State},
3    error::{ArgumentError, Error, Result},
4    names::{register_index, register_name},
5    ContractId, RunResult, Transaction,
6};
7use fuel_tx::Receipt;
8use fuel_vm::consts::{VM_MAX_RAM, VM_REGISTER_COUNT, WORD_SIZE};
9use std::collections::HashSet;
10use strsim::levenshtein;
11use sway_core::asm_generation::ProgramABI;
12
13#[derive(Debug, Clone)]
14pub struct Command {
15    pub name: &'static str,
16    pub aliases: &'static [&'static str],
17    pub help: &'static str,
18}
19
20pub struct Commands {
21    pub tx: Command,
22    pub reset: Command,
23    pub continue_: Command,
24    pub step: Command,
25    pub breakpoint: Command,
26    pub registers: Command,
27    pub memory: Command,
28    pub quit: Command,
29    pub help: Command,
30}
31
32impl Commands {
33    pub const fn new() -> Self {
34        Self {
35            tx: Command {
36                name: "start_tx",
37                aliases: &["n", "tx", "new_tx"],
38                help: "Start a new transaction",
39            },
40            reset: Command {
41                name: "reset",
42                aliases: &[],
43                help: "Reset debugger state",
44            },
45            continue_: Command {
46                name: "continue",
47                aliases: &["c"],
48                help: "Continue execution",
49            },
50            step: Command {
51                name: "step",
52                aliases: &["s"],
53                help: "Step execution",
54            },
55            breakpoint: Command {
56                name: "breakpoint",
57                aliases: &["b"],
58                help: "Set breakpoint",
59            },
60            registers: Command {
61                name: "register",
62                aliases: &["r", "reg", "registers"],
63                help: "View registers",
64            },
65            memory: Command {
66                name: "memory",
67                aliases: &["m", "mem"],
68                help: "View memory",
69            },
70            quit: Command {
71                name: "quit",
72                aliases: &["exit"],
73                help: "Exit debugger",
74            },
75            help: Command {
76                name: "help",
77                aliases: &["h", "?"],
78                help: "Show help for commands",
79            },
80        }
81    }
82
83    pub fn all_commands(&self) -> Vec<&Command> {
84        vec![
85            &self.tx,
86            &self.reset,
87            &self.continue_,
88            &self.step,
89            &self.breakpoint,
90            &self.registers,
91            &self.memory,
92            &self.quit,
93            &self.help,
94        ]
95    }
96
97    pub fn is_tx_command(&self, cmd: &str) -> bool {
98        self.tx.name == cmd || self.tx.aliases.contains(&cmd)
99    }
100
101    pub fn is_register_command(&self, cmd: &str) -> bool {
102        self.registers.name == cmd || self.registers.aliases.contains(&cmd)
103    }
104
105    pub fn is_memory_command(&self, cmd: &str) -> bool {
106        self.memory.name == cmd || self.memory.aliases.contains(&cmd)
107    }
108
109    pub fn is_breakpoint_command(&self, cmd: &str) -> bool {
110        self.breakpoint.name == cmd || self.breakpoint.aliases.contains(&cmd)
111    }
112
113    pub fn is_quit_command(&self, cmd: &str) -> bool {
114        self.quit.name == cmd || self.quit.aliases.contains(&cmd)
115    }
116
117    pub fn is_reset_command(&self, cmd: &str) -> bool {
118        self.reset.name == cmd || self.reset.aliases.contains(&cmd)
119    }
120
121    pub fn is_continue_command(&self, cmd: &str) -> bool {
122        self.continue_.name == cmd || self.continue_.aliases.contains(&cmd)
123    }
124
125    pub fn is_step_command(&self, cmd: &str) -> bool {
126        self.step.name == cmd || self.step.aliases.contains(&cmd)
127    }
128
129    pub fn is_help_command(&self, cmd: &str) -> bool {
130        self.help.name == cmd || self.help.aliases.contains(&cmd)
131    }
132
133    pub fn find_command(&self, name: &str) -> Option<&Command> {
134        self.all_commands()
135            .into_iter()
136            .find(|cmd| cmd.name == name || cmd.aliases.contains(&name))
137    }
138
139    /// Returns a set of all valid command strings including aliases
140    pub fn get_all_command_strings(&self) -> HashSet<&'static str> {
141        let mut commands = HashSet::new();
142        for cmd in self.all_commands() {
143            commands.insert(cmd.name);
144            commands.extend(cmd.aliases);
145        }
146        commands
147    }
148
149    /// Suggests a similar command
150    pub fn find_closest(&self, unknown_cmd: &str) -> Option<&Command> {
151        self.all_commands()
152            .into_iter()
153            .flat_map(|cmd| {
154                std::iter::once((cmd, cmd.name))
155                    .chain(cmd.aliases.iter().map(move |&alias| (cmd, alias)))
156            })
157            .map(|(cmd, name)| (cmd, levenshtein(unknown_cmd, name)))
158            .filter(|&(_, distance)| distance <= 2)
159            .min_by_key(|&(_, distance)| distance)
160            .map(|(cmd, _)| cmd)
161    }
162}
163
164/// Start a debugging session for a transaction with optional ABI support.
165///
166/// Handles two distinct modes of operation:
167/// 1. Local Development: `tx transaction.json abi.json`
168/// 2. Contract-specific: `tx transaction.json --abi <contract_id>:<abi_file.json>`
169///
170/// In both modes, the function will automatically attempt to fetch ABIs for any
171/// contract IDs encountered during execution if they haven't been explicitly provided.
172///
173/// # Arguments format
174/// - First argument: Path to transaction JSON file (required)
175/// - Local dev mode: Optional path to ABI JSON file
176/// - Contract mode: Multiple `--abi contract_id:abi_file.json` pairs
177///
178/// # Example usage
179/// ```text
180/// tx transaction.json                                     // No ABI
181/// tx transaction.json abi.json                           // Local development
182/// tx transaction.json --abi 0x123...:contract.json       // Single contract
183/// tx transaction.json --abi 0x123...:a.json --abi 0x456...:b.json  // Multiple
184/// ```
185pub async fn cmd_start_tx(state: &mut State, mut args: Vec<String>) -> Result<()> {
186    // Remove command name from arguments
187    args.remove(0);
188    ArgumentError::ensure_arg_count(&args, 1, 2)?;
189
190    let mut abi_args = Vec::new();
191    let mut tx_path = None;
192
193    // Parse arguments iteratively, handling both --abi flags and local dev mode
194    let mut i = 0;
195    while i < args.len() {
196        match args[i].as_str() {
197            "--abi" => {
198                if i + 1 < args.len() {
199                    abi_args.push(args[i + 1].clone());
200                    i += 2;
201                } else {
202                    return Err(ArgumentError::Invalid("Missing argument for --abi".into()).into());
203                }
204            }
205            arg => {
206                if tx_path.is_none() {
207                    // First non-flag argument is the transaction path
208                    tx_path = Some(arg.to_string());
209                } else if arg.ends_with(".json") {
210                    // Second .json file is treated as local development ABI
211                    let abi_content = std::fs::read_to_string(arg).map_err(Error::IoError)?;
212                    let fuel_abi =
213                        serde_json::from_str::<fuel_abi_types::abi::program::ProgramABI>(
214                            &abi_content,
215                        )
216                        .map_err(Error::JsonError)?;
217                    state
218                        .contract_abis
219                        .register_abi(ContractId::zeroed(), ProgramABI::Fuel(fuel_abi));
220                }
221                i += 1;
222            }
223        }
224    }
225
226    let tx_path =
227        tx_path.ok_or_else(|| ArgumentError::Invalid("Transaction file required".into()))?;
228
229    // Process contract-specific ABI mappings from --abi arguments
230    for abi_arg in abi_args {
231        if let Some((contract_id, abi_path)) = abi_arg.split_once(':') {
232            let contract_id = contract_id.parse::<ContractId>().map_err(|_| {
233                ArgumentError::Invalid(format!("Invalid contract ID: {}", contract_id))
234            })?;
235
236            let abi_content = std::fs::read_to_string(abi_path).map_err(Error::IoError)?;
237            let fuel_abi =
238                serde_json::from_str::<fuel_abi_types::abi::program::ProgramABI>(&abi_content)
239                    .map_err(Error::JsonError)?;
240
241            state
242                .contract_abis
243                .register_abi(contract_id, ProgramABI::Fuel(fuel_abi));
244        } else {
245            return Err(
246                ArgumentError::Invalid(format!("Invalid --abi argument: {}", abi_arg)).into(),
247            );
248        }
249    }
250
251    // Start transaction execution
252    let tx_json = std::fs::read(&tx_path).map_err(Error::IoError)?;
253    let tx: Transaction = serde_json::from_slice(&tx_json).map_err(Error::JsonError)?;
254
255    let status = state
256        .client
257        .start_tx(&state.session_id, &tx)
258        .await
259        .map_err(|e| Error::FuelClientError(e.to_string()))?;
260
261    pretty_print_run_result(&status, state);
262    Ok(())
263}
264
265pub async fn cmd_reset(state: &mut State, mut args: Vec<String>) -> Result<()> {
266    args.remove(0); // Remove the command name
267    ArgumentError::ensure_arg_count(&args, 0, 0)?; // Ensure no extra arguments
268
269    // Reset the session
270    state
271        .client
272        .reset(&state.session_id)
273        .await
274        .map_err(|e| Error::FuelClientError(e.to_string()))?;
275
276    Ok(())
277}
278
279pub async fn cmd_continue(state: &mut State, mut args: Vec<String>) -> Result<()> {
280    args.remove(0); // Remove the command name
281    ArgumentError::ensure_arg_count(&args, 0, 0)?; // Ensure no extra arguments
282
283    // Continue the transaction
284    let status = state
285        .client
286        .continue_tx(&state.session_id)
287        .await
288        .map_err(|e| Error::FuelClientError(e.to_string()))?;
289
290    pretty_print_run_result(&status, state);
291    Ok(())
292}
293
294pub async fn cmd_step(state: &mut State, mut args: Vec<String>) -> Result<()> {
295    args.remove(0); // Remove the command name
296    ArgumentError::ensure_arg_count(&args, 0, 1)?; // Ensure the argument count is at most 1
297
298    // Determine whether to enable or disable single stepping
299    let enable = args
300        .first()
301        .is_none_or(|v| !["off", "no", "disable"].contains(&v.as_str()));
302
303    // Call the client
304    state
305        .client
306        .set_single_stepping(&state.session_id, enable)
307        .await
308        .map_err(|e| Error::FuelClientError(e.to_string()))?;
309
310    Ok(())
311}
312
313pub async fn cmd_breakpoint(state: &mut State, mut args: Vec<String>) -> Result<()> {
314    args.remove(0); // Remove command name
315    ArgumentError::ensure_arg_count(&args, 1, 2)?;
316
317    let offset_str = args.pop().unwrap(); // Safe due to arg count check
318    let offset = parse_int(&offset_str).ok_or(ArgumentError::InvalidNumber(offset_str))?;
319
320    let contract = if let Some(contract_id) = args.pop() {
321        contract_id
322            .parse::<ContractId>()
323            .map_err(|_| ArgumentError::Invalid(format!("Invalid contract ID: {}", contract_id)))?
324    } else {
325        ContractId::zeroed()
326    };
327
328    // Call client
329    state
330        .client
331        .set_breakpoint(&state.session_id, contract, offset as u64)
332        .await
333        .map_err(|e| Error::FuelClientError(e.to_string()))?;
334
335    Ok(())
336}
337
338pub async fn cmd_registers(state: &mut State, mut args: Vec<String>) -> Result<()> {
339    args.remove(0); // Remove the command name
340
341    if args.is_empty() {
342        // Print all registers
343        for r in 0..VM_REGISTER_COUNT {
344            let value = state
345                .client
346                .register(&state.session_id, r as u32)
347                .await
348                .map_err(|e| Error::FuelClientError(e.to_string()))?;
349            println!("reg[{:#x}] = {:<8} # {}", r, value, register_name(r));
350        }
351    } else {
352        // Process specific registers provided as arguments
353        for arg in &args {
354            if let Some(v) = parse_int(arg) {
355                if v < VM_REGISTER_COUNT {
356                    let value = state
357                        .client
358                        .register(&state.session_id, v as u32)
359                        .await
360                        .map_err(|e| Error::FuelClientError(e.to_string()))?;
361                    println!("reg[{:#02x}] = {:<8} # {}", v, value, register_name(v));
362                } else {
363                    return Err(ArgumentError::InvalidNumber(format!(
364                        "Register index too large: {v}"
365                    ))
366                    .into());
367                }
368            } else if let Some(index) = register_index(arg) {
369                let value = state
370                    .client
371                    .register(&state.session_id, index as u32)
372                    .await
373                    .map_err(|e| Error::FuelClientError(e.to_string()))?;
374                println!("reg[{index:#02x}] = {value:<8} # {arg}");
375            } else {
376                return Err(ArgumentError::Invalid(format!("Unknown register name: {arg}")).into());
377            }
378        }
379    }
380    Ok(())
381}
382
383pub async fn cmd_memory(state: &mut State, mut args: Vec<String>) -> Result<()> {
384    args.remove(0); // Remove the command name
385
386    // Parse limit argument or use the default
387    let limit = args
388        .pop()
389        .map(|a| parse_int(&a).ok_or(ArgumentError::InvalidNumber(a)))
390        .transpose()?
391        .unwrap_or(WORD_SIZE * (VM_MAX_RAM as usize));
392
393    // Parse offset argument or use the default
394    let offset = args
395        .pop()
396        .map(|a| parse_int(&a).ok_or(ArgumentError::InvalidNumber(a)))
397        .transpose()?
398        .unwrap_or(0);
399
400    // Ensure the argument count is at most 2
401    ArgumentError::ensure_arg_count(&args, 0, 2)?;
402
403    // Fetch memory from the client
404    let mem = state
405        .client
406        .memory(&state.session_id, offset as u32, limit as u32)
407        .await
408        .map_err(|e| Error::FuelClientError(e.to_string()))?;
409
410    // Print memory contents
411    for (i, chunk) in mem.chunks(WORD_SIZE).enumerate() {
412        print!(" {:06x}:", offset + i * WORD_SIZE);
413        for byte in chunk {
414            print!(" {byte:02x}");
415        }
416        println!();
417    }
418    Ok(())
419}
420
421// Add help command implementation:
422pub async fn cmd_help(helper: &DebuggerHelper, args: &[String]) -> Result<()> {
423    if args.len() > 1 {
424        // Help for specific command
425        if let Some(cmd) = helper.commands.find_command(&args[1]) {
426            println!("{} - {}", cmd.name, cmd.help);
427            if !cmd.aliases.is_empty() {
428                println!("Aliases: {}", cmd.aliases.join(", "));
429            }
430            return Ok(());
431        }
432        println!("Unknown command: '{}'", args[1]);
433    }
434
435    println!("Available commands:");
436    for cmd in helper.commands.all_commands() {
437        println!("  {:<12} - {}", cmd.name, cmd.help);
438        if !cmd.aliases.is_empty() {
439            println!("    aliases: {}", cmd.aliases.join(", "));
440        }
441    }
442    Ok(())
443}
444
445/// Pretty-prints the result of a run, including receipts and breakpoint information.
446///
447/// Outputs each receipt in the `RunResult` and details about the breakpoint if present.
448/// If the execution terminated without hitting a breakpoint, it prints "Terminated".
449fn pretty_print_run_result(rr: &RunResult, state: &mut State) {
450    for receipt in rr.receipts() {
451        println!("Receipt: {receipt:?}");
452
453        if let Receipt::LogData {
454            id,
455            rb,
456            data: Some(data),
457            ..
458        } = receipt
459        {
460            // If the ABI is available, decode the log data
461            if let Some(abi) = state.contract_abis.get_or_fetch_abi(&id) {
462                if let Ok(decoded_log_data) =
463                    forc_util::tx_utils::decode_log_data(&rb.to_string(), &data, abi)
464                {
465                    println!(
466                        "Decoded log value: {}, from contract: {}",
467                        decoded_log_data.value, id
468                    );
469                }
470            }
471        }
472    }
473    if let Some(bp) = &rr.breakpoint {
474        println!(
475            "Stopped on breakpoint at address {} of contract {}",
476            bp.pc.0, bp.contract
477        );
478    } else {
479        println!("Terminated");
480    }
481}
482
483/// Parses a string representing a number and returns it as a `usize`.
484///
485/// The input string can be in decimal or hexadecimal format:
486/// - Decimal numbers are parsed normally (e.g., `"123"`).
487/// - Hexadecimal numbers must be prefixed with `"0x"` (e.g., `"0x7B"`).
488/// - Underscores can be used as visual separators (e.g., `"1_000"` or `"0x1_F4"`).
489///
490/// If the input string is not a valid number in the specified format, `None` is returned.
491///
492/// # Examples
493///
494/// ```
495/// use forc_debug::cli::parse_int;
496/// /// Use underscores as separators in decimal and hexadecimal numbers
497/// assert_eq!(parse_int("123"), Some(123));
498/// assert_eq!(parse_int("1_000"), Some(1000));
499///
500/// /// Parse hexadecimal numbers with "0x" prefix
501/// assert_eq!(parse_int("0x7B"), Some(123));
502/// assert_eq!(parse_int("0x1_F4"), Some(500));
503///
504/// /// Handle invalid inputs gracefully
505/// assert_eq!(parse_int("abc"), None);
506/// assert_eq!(parse_int("0xZZZ"), None);
507/// assert_eq!(parse_int(""), None);
508/// ```
509///
510/// # Errors
511///
512/// Returns `None` if the input string contains invalid characters,
513/// is not properly formatted, or cannot be parsed into a `usize`.
514pub fn parse_int(s: &str) -> Option<usize> {
515    let (s, radix) = s.strip_prefix("0x").map_or((s, 10), |s| (s, 16));
516    usize::from_str_radix(&s.replace('_', ""), radix).ok()
517}