Skip to content

Commit 936e752

Browse files
authored
feat: forc-call abi backtracing (#7502)
## Description This PR introduces panic/error traces to the forc call trace output. This functionality is built on top of the existing abi-backtracing introduced in the following: - https://github.com/FuelLabs/sway-rfcs/blob/master/rfcs/0016-abi-backtracing.md - #7224 - #7277 - FuelLabs/fuel-abi-types#36 - https://github.com/FuelLabs/fuel-abi-types/pull/40/files Supplying verbosity level greater than 1 (i.e. `-vv` or `-v=2`) will display the panic/error traces when using `forc-call`. If the called function panics, the panic message and the full backtrace will be displayed in the trace output. ## Example <details> <summary>Example contract code</summary> ```sway contract; abi AbiErrorDemo { #[storage(write)] fn write_non_zero(value: u64); #[storage(read)] fn read_value() -> u64; } storage { value: u64 = 0, } #[error_type] pub enum PanicError { #[error(m = "The provided value must be greater than zero.")] ZeroValue: (), } impl AbiErrorDemo for Contract { #[storage(write)] fn write_non_zero(value: u64) { set_non_zero_value(value); } #[storage(read)] fn read_value() -> u64 { storage.value.read() } } #[trace(always)] #[storage(write)] fn set_non_zero_value(value: u64) { ensure_non_zero(value); storage.value.write(value); } #[trace(always)] fn ensure_non_zero(value: u64) { ensure_non_zero_impl(value); } #[trace(always)] fn ensure_non_zero_impl(value: u64) { if value == 0 { panic PanicError::ZeroValue; } } ``` </details> Example Call: ```sh cargo run -p forc-client --bin forc-call -- \ --abi out/debug/abi_errors-abi.json \ babdc125da45eac42309e60d3aea63a53843f5ff2438d1a88bf8c788e8348c58 \ write_non_zero "0" -vv ``` Example Output: <img width="857" height="340" alt="Screenshot 2025-11-24 at 8 36 00 PM" src="https://github.com/user-attachments/assets/9a14053e-9318-4543-a01a-0d794e30dff2" /> ## Checklist - [ ] I have linked to any relevant issues. - [ ] I have commented my code, particularly in hard-to-understand areas. - [ ] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [ ] If my change requires substantial documentation changes, I have [requested support from the DevRel team](https://github.com/FuelLabs/devrel-requests/issues/new/choose) - [ ] I have added tests that prove my fix is effective or that my feature works. - [ ] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [ ] I have done my best to ensure that my PR adheres to [the Fuel Labs Code Review Standards](https://github.com/FuelLabs/rfcs/blob/master/text/code-standards/external-contributors.md). - [ ] I have requested a review from the relevant team or maintainers. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Adds ABI-aware revert decoding to forc-call, displaying panic messages, values, and backtraces in verbose traces and surfacing revert errors early. > > - **forc-client (call/trace)**: > - Add ABI-aware revert decoding (`RevertInfoSummary`) and integrate into `TraceEvent::Revert`; render panic message, value, location, and backtrace in `display_transaction_trace`. > - New helpers: `decode_revert_info` and `first_revert_info` to extract revert details from receipts and trace. > - `call_function`: generate receipts once, include in interpreter, display detailed info on verbosity, and return an error early when a revert is detected; parse outputs after. > - Minor: reference `trace::display_transaction_trace` directly; extend tests to cover revert detail rendering. > - **forc-util**: > - Add `revert_info_from_receipts` to build `RevertInfo` (revert code, panic metadata) from receipts using optional ABI. > - **forc-test**: > - Replace `revert_code()` with `revert_info()` using new utility; filter by actual revert code; simplify API. > - **Docs**: > - Add "Seeing revert information and backtraces" section with example usage/output for `forc call -vv`. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 957d411. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Co-authored-by: z <[email protected]>
1 parent 0682209 commit 936e752

File tree

6 files changed

+535
-58
lines changed

6 files changed

+535
-58
lines changed

docs/book/src/testing/testing_with_forc_call.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,37 @@ forc call <CONTRACT_ID> --abi <PATH> <FUNCTION> --wallet
199199
forc call <CONTRACT_ID> --abi <PATH> <FUNCTION> --signing-key <KEY>
200200
```
201201

202+
### Seeing revert information and backtraces
203+
204+
If the contract ABI contains `errorCodes` and `panickingCalls` (generated by `panic`-aware builds), `forc call` will display rich revert information — panic message, panic value, and backtrace — inline in the trace output. Run with verbosity to see it:
205+
206+
```bash
207+
forc call <CONTRACT_ID> \
208+
--abi ./out/debug/my_contract-abi.json \
209+
write_non_zero 0 -vv
210+
```
211+
212+
Example (formatted):
213+
214+
```text
215+
[Script]
216+
├─ [1017] <contract>::write_non_zero(0)
217+
│ ├─ emit ZeroValue
218+
│ └─ ← [Revert]
219+
│ ├─ revert code: 8000000000001001
220+
│ ├─ panic message: The provided value must be greater than zero.
221+
│ ├─ panic value: ZeroValue
222+
│ ├─ panicked: in my_contract::ensure_non_zero
223+
│ │ └─ at src/main.sw:45:9
224+
│ └─ backtrace: called in my_contract::set_non_zero_value
225+
│ └─ at src/main.sw:38:5
226+
│ called in <Contract as AbiErrorDemo>::write_non_zero
227+
│ └─ at src/main.sw:26:9
228+
[ScriptResult] result: Revert, gas_used: 784
229+
```
230+
231+
If the ABI does not include those sections, only the raw revert code is displayed.
232+
202233
### Asset Transfers
203234

204235
```sh

forc-plugins/forc-client/src/op/call/call_function.rs

Lines changed: 34 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -284,29 +284,11 @@ pub async fn call_function(
284284
let fuel_tx::Transaction::Script(script) = &tx else {
285285
bail!("Transaction is not a script");
286286
};
287-
288-
// Parse the result based on output format
289-
let mut receipt_parser =
290-
ReceiptParser::new(tx_execution.result.receipts(), DecoderConfig::default());
291-
let result = match output {
292-
cmd::call::OutputFormat::Default | cmd::call::OutputFormat::Json => {
293-
let data = receipt_parser
294-
.extract_contract_call_data(contract_id)
295-
.ok_or(anyhow!("Failed to extract contract call data"))?;
296-
ABIDecoder::default()
297-
.decode_as_debug_str(&output_param, data.as_slice())
298-
.map_err(|e| anyhow!("Failed to decode as debug string: {e}"))?
299-
}
300-
cmd::call::OutputFormat::Raw => {
301-
let token = receipt_parser
302-
.parse_call(contract_id, &output_param)
303-
.map_err(|e| anyhow!("Failed to parse call data: {e}"))?;
304-
token_to_string(&token)
305-
.map_err(|e| anyhow!("Failed to convert token to string: {e}"))?
306-
}
307-
};
287+
let receipts = tx_execution.result.receipts();
308288

309289
// Generate execution trace events by stepping through VM interpreter
290+
#[cfg(test)]
291+
let trace_events: Vec<crate::op::call::trace::TraceEvent> = vec![];
310292
#[cfg(not(test))]
311293
let trace_events = {
312294
use crate::op::call::trace::interpret_execution_trace;
@@ -315,17 +297,14 @@ pub async fn call_function(
315297
&mode,
316298
&consensus_params,
317299
script,
318-
tx_execution.result.receipts(),
300+
receipts,
319301
storage_reads,
320302
&abi_map,
321303
)
322304
.await
323305
.map_err(|e| anyhow!("Failed to generate execution trace: {e}"))?
324306
};
325307

326-
#[cfg(test)]
327-
let trace_events = vec![];
328-
329308
// display detailed call info if verbosity is set
330309
if cmd.verbosity > 0 {
331310
// Convert labels from Vec to HashMap
@@ -346,6 +325,36 @@ pub async fn call_function(
346325
)?;
347326
}
348327

328+
// If the call reverted, exit early; return an error with the revert details
329+
if let Some((contract_id, revert_info)) =
330+
crate::op::call::trace::first_revert_info(&trace_events)
331+
{
332+
return Err(anyhow!(
333+
"Contract 0x{contract_id} reverted with code 0x{:x}",
334+
revert_info.revert_code
335+
));
336+
}
337+
338+
// Parse the result based on output format
339+
let mut receipt_parser = ReceiptParser::new(receipts, DecoderConfig::default());
340+
let result = match output {
341+
cmd::call::OutputFormat::Default | cmd::call::OutputFormat::Json => {
342+
let data = receipt_parser
343+
.extract_contract_call_data(contract_id)
344+
.ok_or(anyhow!("Failed to extract contract call data"))?;
345+
ABIDecoder::default()
346+
.decode_as_debug_str(&output_param, data.as_slice())
347+
.map_err(|e| anyhow!("Failed to decode as debug string: {e}"))?
348+
}
349+
cmd::call::OutputFormat::Raw => {
350+
let token = receipt_parser
351+
.parse_call(contract_id, &output_param)
352+
.map_err(|e| anyhow!("Failed to parse call data: {e}"))?;
353+
token_to_string(&token)
354+
.map_err(|e| anyhow!("Failed to convert token to string: {e}"))?
355+
}
356+
};
357+
349358
// display tx info
350359
super::display_tx_info(
351360
tx_execution.id.to_string(),

forc-plugins/forc-client/src/op/call/mod.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ use crate::{
1010
cmd,
1111
constants::DEFAULT_PRIVATE_KEY,
1212
op::call::{
13-
call_function::call_function, list_functions::list_contract_functions,
14-
trace::display_transaction_trace, transfer::transfer,
13+
call_function::call_function, list_functions::list_contract_functions, transfer::transfer,
1514
},
1615
util::tx::{prompt_forc_wallet_password, select_local_wallet_account},
1716
};
@@ -286,7 +285,7 @@ pub(crate) fn display_detailed_call_info(
286285
forc_tracing::println_label_green("receipts:", &formatted_receipts);
287286
}
288287
if verbosity >= 2 {
289-
display_transaction_trace(*tx.result.total_gas(), trace_events, labels, writer)
288+
trace::display_transaction_trace(*tx.result.total_gas(), trace_events, labels, writer)
290289
.map_err(|e| anyhow!("Failed to display transaction trace: {e}"))?;
291290
}
292291
if verbosity >= 1 {

0 commit comments

Comments
 (0)