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 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 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 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 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 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 }
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 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 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 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 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 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 pub fn session_id(&self) -> &str {
298 &self.session_id
299 }
300}
301
302#[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}