forc_debug/debugger/
mod.rs

1pub mod commands;
2
3pub use commands::{AbiMapping, BreakpointHit, DebugCommand, DebugResponse, RegisterValue};
4
5use crate::{
6    error::{Error, Result},
7    names::register_name,
8    types::AbiMap,
9    ContractId, FuelClient, RunResult, Transaction,
10};
11use fuel_tx::Receipt;
12use fuel_vm::consts::{VM_REGISTER_COUNT, WORD_SIZE};
13use sway_core::asm_generation::ProgramABI;
14
15pub struct Debugger {
16    client: FuelClient,
17    session_id: String,
18    contract_abis: AbiMap,
19}
20
21impl Debugger {
22    /// Create a debugger instance connected to the given API URL
23    pub async fn new(api_url: &str) -> Result<Self> {
24        let client = FuelClient::new(api_url).map_err(|e| Error::FuelClientError(e.to_string()))?;
25        Self::from_client(client).await
26    }
27
28    /// Create a debugger instance from FuelClient
29    pub async fn from_client(client: FuelClient) -> Result<Self> {
30        let session_id = client
31            .start_session()
32            .await
33            .map_err(|e| Error::FuelClientError(e.to_string()))?;
34
35        Ok(Self {
36            client,
37            session_id,
38            contract_abis: AbiMap::default(),
39        })
40    }
41
42    /// Execute a debugger command from CLI arguments
43    pub async fn execute_from_args<W: std::io::Write>(
44        &mut self,
45        args: Vec<String>,
46        writer: &mut W,
47    ) -> Result<()> {
48        let command = DebugCommand::from_cli_args(&args)?;
49        let response = self.execute(command).await?;
50        match response {
51            DebugResponse::RunResult {
52                receipts,
53                breakpoint,
54            } => {
55                // Process receipts with ABI decoding
56                let decoded_receipts = self.process_receipts(&receipts);
57                for decoded in decoded_receipts {
58                    match decoded {
59                        DecodedReceipt::Regular(receipt) => {
60                            writeln!(writer, "Receipt: {receipt:?}")?;
61                        }
62                        DecodedReceipt::LogData {
63                            receipt,
64                            decoded_value,
65                            contract_id,
66                        } => {
67                            writeln!(writer, "Receipt: {receipt:?}")?;
68                            if let Some(value) = decoded_value {
69                                writeln!(
70                                    writer,
71                                    "Decoded log value: {value}, from contract: {contract_id}"
72                                )?;
73                            }
74                        }
75                    }
76                }
77                // Print breakpoint info
78                if let Some(bp) = breakpoint {
79                    writeln!(
80                        writer,
81                        "Stopped on breakpoint at address {} of contract 0x{}",
82                        bp.pc, bp.contract
83                    )?;
84                } else {
85                    writeln!(writer, "Terminated")?;
86                }
87            }
88            DebugResponse::Success => {
89                // Command completed successfully, no output needed
90            }
91            DebugResponse::Registers(registers) => {
92                for reg in registers {
93                    writeln!(
94                        writer,
95                        "reg[{:#02x}] = {:<8} # {}",
96                        reg.index, reg.value, reg.name
97                    )?;
98                }
99            }
100            DebugResponse::Memory(mem) => {
101                for (i, chunk) in mem.chunks(WORD_SIZE).enumerate() {
102                    write!(writer, " {:06x}:", i * WORD_SIZE)?;
103                    for byte in chunk {
104                        write!(writer, " {byte:02x}")?;
105                    }
106                    writeln!(writer)?;
107                }
108            }
109            DebugResponse::Error(err) => {
110                writeln!(writer, "Error: {err}")?;
111            }
112        }
113        Ok(())
114    }
115
116    pub async fn execute(&mut self, command: DebugCommand) -> Result<DebugResponse> {
117        match command {
118            DebugCommand::StartTransaction {
119                tx_path,
120                abi_mappings,
121            } => self.start_transaction(tx_path, abi_mappings).await,
122            DebugCommand::Reset => self.reset().await,
123            DebugCommand::Continue => self.continue_execution().await,
124            DebugCommand::SetSingleStepping { enable } => self.set_single_stepping(enable).await,
125            DebugCommand::SetBreakpoint {
126                contract_id,
127                offset,
128            } => self.set_breakpoint(contract_id, offset).await,
129            DebugCommand::GetRegisters { indices } => self.get_registers(indices).await,
130            DebugCommand::GetMemory { offset, limit } => self.get_memory(offset, limit).await,
131            DebugCommand::Quit => Ok(DebugResponse::Success),
132        }
133    }
134
135    /// Start a new transaction with optional ABI support
136    async fn start_transaction(
137        &mut self,
138        tx_path: String,
139        abi_mappings: Vec<AbiMapping>,
140    ) -> Result<DebugResponse> {
141        let load_and_parse_abi = |abi_path: &str| -> Result<ProgramABI> {
142            let abi_content = std::fs::read_to_string(abi_path)?;
143            let fuel_abi =
144                serde_json::from_str::<fuel_abi_types::abi::program::ProgramABI>(&abi_content)
145                    .map_err(Error::JsonError)?;
146            Ok(ProgramABI::Fuel(fuel_abi))
147        };
148
149        // Process ABI mappings
150        for mapping in abi_mappings {
151            match mapping {
152                AbiMapping::Local { abi_path } => {
153                    let abi = load_and_parse_abi(&abi_path)?;
154                    self.contract_abis.register_abi(ContractId::zeroed(), abi);
155                }
156                AbiMapping::Contract {
157                    contract_id,
158                    abi_path,
159                } => {
160                    let abi = load_and_parse_abi(&abi_path)?;
161                    self.contract_abis.register_abi(contract_id, abi);
162                }
163            }
164        }
165
166        // Load and start transaction
167        let tx_json = std::fs::read(&tx_path)?;
168        let tx: Transaction = serde_json::from_slice(&tx_json).map_err(Error::JsonError)?;
169
170        let status = self
171            .client
172            .start_tx(&self.session_id, &tx)
173            .await
174            .map_err(|e| Error::FuelClientError(e.to_string()))?;
175
176        Ok(self.create_run_result_response(&status))
177    }
178
179    async fn reset(&mut self) -> Result<DebugResponse> {
180        self.client
181            .reset(&self.session_id)
182            .await
183            .map_err(|e| Error::FuelClientError(e.to_string()))?;
184        Ok(DebugResponse::Success)
185    }
186
187    async fn continue_execution(&mut self) -> Result<DebugResponse> {
188        let status = self
189            .client
190            .continue_tx(&self.session_id)
191            .await
192            .map_err(|e| Error::FuelClientError(e.to_string()))?;
193        Ok(self.create_run_result_response(&status))
194    }
195
196    async fn set_single_stepping(&mut self, enable: bool) -> Result<DebugResponse> {
197        self.client
198            .set_single_stepping(&self.session_id, enable)
199            .await
200            .map_err(|e| Error::FuelClientError(e.to_string()))?;
201        Ok(DebugResponse::Success)
202    }
203
204    async fn set_breakpoint(
205        &mut self,
206        contract_id: ContractId,
207        offset: u64,
208    ) -> Result<DebugResponse> {
209        self.client
210            .set_breakpoint(&self.session_id, contract_id, offset)
211            .await
212            .map_err(|e| Error::FuelClientError(e.to_string()))?;
213        Ok(DebugResponse::Success)
214    }
215
216    async fn get_registers(&mut self, indices: Vec<u32>) -> Result<DebugResponse> {
217        let indices = if indices.is_empty() {
218            (0..VM_REGISTER_COUNT as u32).collect()
219        } else {
220            indices
221        };
222
223        let mut values = Vec::new();
224        for index in indices {
225            if index >= VM_REGISTER_COUNT as u32 {
226                return Err(Error::ArgumentError(crate::error::ArgumentError::Invalid(
227                    format!("Register index too large: {index}"),
228                )));
229            }
230            let value = self
231                .client
232                .register(&self.session_id, index)
233                .await
234                .map_err(|e| Error::FuelClientError(e.to_string()))?;
235            values.push(RegisterValue {
236                index,
237                value,
238                name: register_name(index as usize).to_string(),
239            });
240        }
241        Ok(DebugResponse::Registers(values))
242    }
243
244    async fn get_memory(&mut self, offset: u32, limit: u32) -> Result<DebugResponse> {
245        let mem = self
246            .client
247            .memory(&self.session_id, offset, limit)
248            .await
249            .map_err(|e| Error::FuelClientError(e.to_string()))?;
250        Ok(DebugResponse::Memory(mem))
251    }
252
253    /// Convert RunResult to DebugResponse
254    fn create_run_result_response(&self, run_result: &RunResult) -> DebugResponse {
255        let receipts: Vec<Receipt> = run_result.receipts().collect();
256        let breakpoint = run_result.breakpoint.as_ref().map(|bp| BreakpointHit {
257            contract: bp.contract.clone().into(),
258            pc: bp.pc.0,
259        });
260        DebugResponse::RunResult {
261            receipts,
262            breakpoint,
263        }
264    }
265
266    /// Process receipts with ABI decoding (used for pretty printing in CLI)
267    pub fn process_receipts(&mut self, receipts: &[Receipt]) -> Vec<DecodedReceipt> {
268        receipts
269            .iter()
270            .map(|receipt| {
271                if let Receipt::LogData {
272                    id,
273                    rb,
274                    data: Some(data),
275                    ..
276                } = receipt
277                {
278                    self.contract_abis
279                        .get_or_fetch_abi(id)
280                        .and_then(|abi| {
281                            forc_util::tx_utils::decode_log_data(&rb.to_string(), data, abi).ok()
282                        })
283                        .map(|decoded_log| DecodedReceipt::LogData {
284                            receipt: receipt.clone(),
285                            decoded_value: Some(decoded_log.value),
286                            contract_id: *id,
287                        })
288                        .unwrap_or_else(|| DecodedReceipt::Regular(receipt.clone()))
289                } else {
290                    DecodedReceipt::Regular(receipt.clone())
291                }
292            })
293            .collect()
294    }
295
296    /// Get the current session ID
297    pub fn session_id(&self) -> &str {
298        &self.session_id
299    }
300}
301
302/// Decoded receipt for pretty printing
303#[derive(Debug, Clone)]
304pub enum DecodedReceipt {
305    Regular(Receipt),
306    LogData {
307        receipt: Receipt,
308        decoded_value: Option<String>,
309        contract_id: ContractId,
310    },
311}