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 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 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
164pub async fn cmd_start_tx(state: &mut State, mut args: Vec<String>) -> Result<()> {
186 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 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 tx_path = Some(arg.to_string());
209 } else if arg.ends_with(".json") {
210 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 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 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); ArgumentError::ensure_arg_count(&args, 0, 0)?; 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); ArgumentError::ensure_arg_count(&args, 0, 0)?; 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); ArgumentError::ensure_arg_count(&args, 0, 1)?; let enable = args
300 .first()
301 .is_none_or(|v| !["off", "no", "disable"].contains(&v.as_str()));
302
303 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); ArgumentError::ensure_arg_count(&args, 1, 2)?;
316
317 let offset_str = args.pop().unwrap(); 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 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); if args.is_empty() {
342 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 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); 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 let offset = args
395 .pop()
396 .map(|a| parse_int(&a).ok_or(ArgumentError::InvalidNumber(a)))
397 .transpose()?
398 .unwrap_or(0);
399
400 ArgumentError::ensure_arg_count(&args, 0, 2)?;
402
403 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 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
421pub async fn cmd_help(helper: &DebuggerHelper, args: &[String]) -> Result<()> {
423 if args.len() > 1 {
424 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
445fn 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 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
483pub 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}