1use crate::{error::ArgumentError, ContractId};
2use fuel_tx::Receipt;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub enum DebugCommand {
8 StartTransaction {
10 tx_path: String,
12 abi_mappings: Vec<AbiMapping>,
15 },
16 Reset,
18 Continue,
20 SetSingleStepping {
22 enable: bool,
24 },
25 SetBreakpoint {
27 contract_id: ContractId,
29 offset: u64,
31 },
32 GetRegisters {
34 indices: Vec<u32>,
36 },
37 GetMemory {
39 offset: u32,
41 limit: u32,
43 },
44 Quit,
46}
47
48#[derive(Debug, Clone, Serialize, Deserialize)]
50pub enum AbiMapping {
51 Local { abi_path: String },
53 Contract {
55 contract_id: ContractId,
56 abi_path: String,
57 },
58}
59
60#[derive(Debug, Clone, Serialize, Deserialize)]
62pub enum DebugResponse {
63 RunResult {
65 receipts: Vec<Receipt>,
66 breakpoint: Option<BreakpointHit>,
67 },
68 Success,
70 Registers(Vec<RegisterValue>),
72 Memory(Vec<u8>),
74 Error(String),
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
80pub struct BreakpointHit {
81 pub contract: ContractId,
82 pub pc: u64,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87pub struct RegisterValue {
88 pub index: u32,
89 pub value: u64,
90 pub name: String,
91}
92
93impl DebugCommand {
94 pub fn from_cli_args(args: &[String]) -> Result<Self, ArgumentError> {
96 if args.is_empty() {
97 return Err(ArgumentError::NotEnough {
98 expected: 1,
99 got: 0,
100 });
101 }
102
103 let cmd = &args[0];
104 let args = &args[1..];
105
106 match cmd.as_str() {
107 "start_tx" | "n" | "tx" | "new_tx" => {
108 Self::parse_start_tx(args).map_err(ArgumentError::Invalid)
109 }
110 "reset" => {
111 if !args.is_empty() {
112 return Err(ArgumentError::Invalid(
113 "reset command takes no arguments".to_string(),
114 ));
115 }
116 Ok(DebugCommand::Reset)
117 }
118 "continue" | "c" => {
119 if !args.is_empty() {
120 return Err(ArgumentError::Invalid(
121 "continue command takes no arguments".to_string(),
122 ));
123 }
124 Ok(DebugCommand::Continue)
125 }
126 "step" | "s" => Self::parse_step(args).map_err(ArgumentError::Invalid),
127 "breakpoint" | "bp" | "b" => {
128 Self::parse_breakpoint(args).map_err(ArgumentError::Invalid)
129 }
130 "register" | "r" | "reg" | "registers" => {
131 Self::parse_registers(args).map_err(ArgumentError::Invalid)
132 }
133 "memory" | "m" | "mem" => Self::parse_memory(args).map_err(ArgumentError::Invalid),
134 "quit" | "q" | "exit" => Ok(DebugCommand::Quit),
135 _ => Err(ArgumentError::UnknownCommand(cmd.to_string())),
136 }
137 }
138
139 fn parse_start_tx(args: &[String]) -> Result<Self, String> {
145 if args.is_empty() {
146 return Err("Transaction file path required".to_string());
147 }
148
149 let tx_path = args[0].clone();
150 let mut abi_mappings = Vec::new();
151 let mut i = 1;
152
153 while i < args.len() {
154 if args[i] == "--abi" {
155 if i + 1 >= args.len() {
156 return Err("Missing argument for --abi".to_string());
157 }
158 let abi_arg = &args[i + 1];
159 if let Some((contract_id, abi_path)) = abi_arg.split_once(':') {
160 let contract_id = contract_id
161 .parse::<ContractId>()
162 .map_err(|_| format!("Invalid contract ID: {contract_id}"))?;
163 abi_mappings.push(AbiMapping::Contract {
164 contract_id,
165 abi_path: abi_path.to_string(),
166 });
167 } else {
168 return Err(format!("Invalid --abi argument: {abi_arg}"));
169 }
170 i += 2;
171 } else if args[i].ends_with(".json") {
172 abi_mappings.push(AbiMapping::Local {
174 abi_path: args[i].clone(),
175 });
176 i += 1;
177 } else {
178 return Err(format!("Unexpected argument: {}", args[i]));
179 }
180 }
181
182 Ok(DebugCommand::StartTransaction {
183 tx_path,
184 abi_mappings,
185 })
186 }
187
188 fn parse_step(args: &[String]) -> Result<Self, String> {
189 let enable = args
190 .first()
191 .is_none_or(|v| !["off", "no", "disable"].contains(&v.as_str()));
192
193 Ok(DebugCommand::SetSingleStepping { enable })
194 }
195
196 fn parse_breakpoint(args: &[String]) -> Result<Self, String> {
197 if args.is_empty() {
198 return Err("Breakpoint offset required".to_string());
199 }
200
201 let (contract_id, offset_str) = if args.len() == 2 {
202 let contract_id = args[0]
204 .parse::<ContractId>()
205 .map_err(|_| format!("Invalid contract ID: {}", args[0]))?;
206 (contract_id, &args[1])
207 } else {
208 (ContractId::zeroed(), &args[0])
210 };
211
212 let offset = crate::cli::parse_int(offset_str)
213 .ok_or_else(|| format!("Invalid offset: {offset_str}"))? as u64;
214
215 Ok(DebugCommand::SetBreakpoint {
216 contract_id,
217 offset,
218 })
219 }
220
221 fn parse_registers(args: &[String]) -> Result<Self, String> {
222 let mut indices = Vec::new();
223 for arg in args {
224 if let Some(v) = crate::cli::parse_int(arg) {
225 indices.push(v as u32);
226 } else if let Some(index) = crate::names::register_index(arg) {
227 indices.push(index as u32);
228 } else {
229 return Err(format!("Unknown register: {arg}"));
230 }
231 }
232 Ok(DebugCommand::GetRegisters { indices })
233 }
234
235 fn parse_memory(args: &[String]) -> Result<Self, String> {
236 use fuel_vm::consts::{VM_MAX_RAM, WORD_SIZE};
237
238 let offset = args
239 .first()
240 .map(|a| crate::cli::parse_int(a).ok_or_else(|| format!("Invalid offset: {a}")))
241 .transpose()?
242 .unwrap_or(0) as u32;
243
244 let limit = args
245 .get(1)
246 .map(|a| crate::cli::parse_int(a).ok_or_else(|| format!("Invalid limit: {a}")))
247 .transpose()?
248 .unwrap_or(WORD_SIZE * (VM_MAX_RAM as usize)) as u32;
249
250 Ok(DebugCommand::GetMemory { offset, limit })
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_start_tx_command() {
260 let args = vec!["start_tx".to_string(), "test.json".to_string()];
261 let result = DebugCommand::from_cli_args(&args).unwrap();
262
263 assert!(matches!(
264 result,
265 DebugCommand::StartTransaction { ref tx_path, ref abi_mappings }
266 if tx_path == "test.json" && abi_mappings.is_empty()
267 ));
268
269 let args = vec!["n".to_string(), "test.json".to_string()];
271 let result = DebugCommand::from_cli_args(&args).unwrap();
272 assert!(matches!(result, DebugCommand::StartTransaction { .. }));
273 }
274
275 #[test]
276 fn test_reset_command() {
277 let args = vec!["reset".to_string()];
278 let result = DebugCommand::from_cli_args(&args).unwrap();
279 assert!(matches!(result, DebugCommand::Reset));
280 }
281
282 #[test]
283 fn test_continue_command() {
284 let args = vec!["continue".to_string()];
285 let result = DebugCommand::from_cli_args(&args).unwrap();
286 assert!(matches!(result, DebugCommand::Continue));
287
288 let args = vec!["c".to_string()];
290 let result = DebugCommand::from_cli_args(&args).unwrap();
291 assert!(matches!(result, DebugCommand::Continue));
292 }
293
294 #[test]
295 fn test_step_command() {
296 let args = vec!["step".to_string()];
297 let result = DebugCommand::from_cli_args(&args).unwrap();
298 assert!(matches!(
299 result,
300 DebugCommand::SetSingleStepping { enable: true }
301 ));
302
303 let args = vec!["step".to_string(), "off".to_string()];
304 let result = DebugCommand::from_cli_args(&args).unwrap();
305 assert!(matches!(
306 result,
307 DebugCommand::SetSingleStepping { enable: false }
308 ));
309
310 let args = vec!["s".to_string()];
312 let result = DebugCommand::from_cli_args(&args).unwrap();
313 assert!(matches!(
314 result,
315 DebugCommand::SetSingleStepping { enable: true }
316 ));
317 }
318
319 #[test]
320 fn test_breakpoint_command() {
321 let args = vec!["breakpoint".to_string(), "100".to_string()];
322 let result = DebugCommand::from_cli_args(&args).unwrap();
323 assert!(matches!(
324 result,
325 DebugCommand::SetBreakpoint { contract_id, offset: 100 }
326 if contract_id == ContractId::zeroed()
327 ));
328
329 let args = vec!["bp".to_string(), "50".to_string()];
331 let result = DebugCommand::from_cli_args(&args).unwrap();
332 assert!(matches!(
333 result,
334 DebugCommand::SetBreakpoint { offset: 50, .. }
335 ));
336 }
337
338 #[test]
339 fn test_register_command() {
340 let args = vec!["register".to_string()];
341 let result = DebugCommand::from_cli_args(&args).unwrap();
342 assert!(matches!(
343 result,
344 DebugCommand::GetRegisters { ref indices }
345 if indices.is_empty()
346 ));
347
348 let args = vec!["reg".to_string(), "0".to_string()];
349 let result = DebugCommand::from_cli_args(&args).unwrap();
350 assert!(matches!(
351 result,
352 DebugCommand::GetRegisters { ref indices }
353 if indices == &vec![0]
354 ));
355 }
356
357 #[test]
358 fn test_memory_command() {
359 let args = vec!["memory".to_string()];
360 let result = DebugCommand::from_cli_args(&args).unwrap();
361 assert!(matches!(
362 result,
363 DebugCommand::GetMemory {
364 offset: 0,
365 limit: _
366 }
367 ));
368
369 let args = vec!["memory".to_string(), "100".to_string(), "200".to_string()];
370 let result = DebugCommand::from_cli_args(&args).unwrap();
371 assert!(matches!(
372 result,
373 DebugCommand::GetMemory {
374 offset: 100,
375 limit: 200
376 }
377 ));
378
379 let args = vec!["m".to_string(), "50".to_string()];
381 let result = DebugCommand::from_cli_args(&args).unwrap();
382 assert!(matches!(result, DebugCommand::GetMemory { offset: 50, .. }));
383 }
384
385 #[test]
386 fn test_quit_command() {
387 let args = vec!["quit".to_string()];
388 let result = DebugCommand::from_cli_args(&args).unwrap();
389 assert!(matches!(result, DebugCommand::Quit));
390
391 let args = vec!["q".to_string()];
393 let result = DebugCommand::from_cli_args(&args).unwrap();
394 assert!(matches!(result, DebugCommand::Quit));
395 }
396
397 #[test]
398 fn test_error_cases() {
399 let args = vec![];
401 let result = DebugCommand::from_cli_args(&args);
402 assert!(matches!(
403 result,
404 Err(ArgumentError::NotEnough {
405 expected: 1,
406 got: 0
407 })
408 ));
409
410 let args = vec!["unknown".to_string()];
412 let result = DebugCommand::from_cli_args(&args);
413 assert!(matches!(result, Err(ArgumentError::UnknownCommand(_))));
414
415 let args = vec!["start_tx".to_string()];
417 let result = DebugCommand::from_cli_args(&args);
418 assert!(result.is_err());
419
420 let args = vec!["breakpoint".to_string()];
421 let result = DebugCommand::from_cli_args(&args);
422 assert!(result.is_err());
423 }
424}