Skip to content

Commit 0a56930

Browse files
authored
forc-debug: Add ABI support for decoding log values (#6856)
## Description Adds support for ABI files in `forc-debug` to decode log values while debugging using the CLI. Users can now: for example: ``` tx tx.json abi.json ``` When the Sway ABI Registry is available, the debugger will automatically fetch ABIs for deployed contracts. Have an issue open to implement this here #6862 Updates documentation to show decoded log output, adds tab completion for ABI files, and refreshes bytecode examples to match current output. The `test_cli` test has been updated to take an ABI and check that the correct decoded value is returned. ## Checklist - [x] I have linked to any relevant issues. - [x] I have commented my code, particularly in hard-to-understand areas. - [x] I have updated the documentation where relevant (API docs, the reference, and the Sway book). - [x] If my change requires substantial documentation changes, I have [requested support from the DevRel team](https://github.com/FuelLabs/devrel-requests/issues/new/choose) - [x] I have added tests that prove my fix is effective or that my feature works. - [x] I have added (or requested a maintainer to add) the necessary `Breaking*` or `New Feature` labels where relevant. - [x] 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). - [x] I have requested a review from the relevant team or maintainers.
1 parent 9df66ce commit 0a56930

File tree

11 files changed

+378
-436
lines changed

11 files changed

+378
-436
lines changed

Cargo.lock

+2
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/book/spell-check-custom-words.txt

+2-1
Original file line numberDiff line numberDiff line change
@@ -236,4 +236,5 @@ semiautomatically
236236
FuelLabs
237237
github
238238
toml
239-
hardcoded
239+
hardcoded
240+
subdirectories

docs/book/src/debugging/debugging_with_cli.md

+64-232
Large diffs are not rendered by default.

forc-plugins/forc-debug/Cargo.toml

+2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ dirs.workspace = true
1616
forc-pkg.workspace = true
1717
forc-test.workspace = true
1818
forc-tracing.workspace = true
19+
fuel-abi-types.workspace = true
1920
fuel-core-client.workspace = true
21+
fuel-tx.workspace = true
2022
fuel-types = { workspace = true, features = ["serde"] }
2123
fuel-vm = { workspace = true, features = ["serde"] }
2224
rayon.workspace = true
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
{
2+
"programType": "script",
3+
"specVersion": "1",
4+
"encodingVersion": "1",
5+
"concreteTypes": [
6+
{
7+
"type": "()",
8+
"concreteTypeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d"
9+
},
10+
{
11+
"type": "u64",
12+
"concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
13+
}
14+
],
15+
"metadataTypes": [],
16+
"functions": [
17+
{
18+
"inputs": [],
19+
"name": "main",
20+
"output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d",
21+
"attributes": null
22+
}
23+
],
24+
"loggedTypes": [
25+
{
26+
"logId": "1515152261580153489",
27+
"concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
28+
}
29+
],
30+
"messagesTypes": [],
31+
"configurables": []
32+
}

forc-plugins/forc-debug/examples/example_tx.json

+3-148
Original file line numberDiff line numberDiff line change
@@ -3,94 +3,14 @@
33
"body": {
44
"script_gas_limit": 1000000,
55
"script": [
6-
144,
7-
0,
8-
0,
9-
4,
10-
71,
11-
0,
12-
0,
13-
0,
14-
0,
15-
0,
16-
0,
17-
0,
18-
0,
19-
0,
20-
0,
21-
68,
22-
93,
23-
252,
24-
192,
25-
1,
26-
16,
27-
255,
28-
243,
29-
0,
30-
26,
31-
72,
32-
16,
33-
0,
34-
26,
35-
68,
36-
0,
37-
0,
38-
93,
39-
67,
40-
240,
41-
0,
42-
22,
43-
65,
44-
20,
45-
0,
46-
115,
47-
64,
48-
0,
49-
13,
50-
51,
51-
72,
52-
0,
53-
0,
54-
36,
55-
0,
56-
0,
57-
0,
58-
16,
59-
69,
60-
16,
61-
64,
62-
27,
63-
73,
64-
36,
65-
64,
66-
144,
67-
0,
68-
0,
69-
8,
70-
71,
71-
0,
72-
0,
73-
0,
74-
0,
75-
0,
76-
0,
77-
0,
78-
0,
79-
0,
80-
0,
81-
5
6+
26, 240, 48, 0, 116, 0, 0, 2, 0, 0, 0, 0, 0, 0, 3, 96, 93, 255, 192, 1, 16, 255, 255, 0, 26, 236, 80, 0, 145, 0, 0, 184, 80, 67, 176, 80, 32, 248, 51, 0, 88, 251, 224, 2, 80, 251, 224, 4, 116, 0, 0, 37, 80, 71, 176, 40, 26, 233, 16, 0, 32, 248, 51, 0, 88, 251, 224, 2, 80, 251, 224, 4, 116, 0, 0, 136, 26, 71, 208, 0, 114, 72, 0, 24, 40, 237, 20, 128, 80, 79, 176, 120, 114, 68, 0, 24, 40, 79, 180, 64, 80, 71, 176, 160, 114, 72, 0, 24, 40, 69, 52, 128, 80, 71, 176, 96, 114, 72, 0, 24, 40, 69, 52, 128, 80, 75, 176, 64, 26, 233, 16, 0, 26, 229, 32, 0, 32, 248, 51, 0, 88, 251, 224, 2, 80, 251, 224, 4, 116, 0, 0, 144, 26, 71, 208, 0, 80, 75, 176, 24, 114, 76, 0, 16, 40, 73, 20, 192, 80, 71, 176, 144, 114, 76, 0, 16, 40, 69, 36, 192, 114, 72, 0, 16, 40, 65, 20, 128, 93, 69, 0, 1, 93, 65, 0, 0, 37, 65, 16, 0, 149, 0, 0, 63, 150, 8, 0, 0, 26, 236, 80, 0, 145, 0, 1, 88, 26, 87, 224, 0, 95, 236, 16, 42, 95, 236, 0, 41, 93, 67, 176, 41, 114, 68, 0, 5, 22, 65, 4, 64, 118, 64, 0, 81, 93, 67, 176, 42, 80, 71, 176, 200, 26, 233, 16, 0, 32, 248, 51, 0, 88, 251, 224, 2, 80, 251, 224, 4, 116, 0, 0, 87, 26, 71, 208, 0, 114, 72, 0, 24, 40, 237, 20, 128, 80, 71, 176, 160, 114, 72, 0, 24, 40, 71, 180, 128, 80, 75, 176, 24, 114, 76, 0, 24, 40, 73, 20, 192, 80, 71, 176, 88, 114, 76, 0, 24, 40, 69, 36, 192, 93, 83, 176, 11, 93, 79, 176, 12, 93, 71, 176, 13, 114, 72, 0, 8, 16, 73, 20, 128, 21, 73, 36, 192, 118, 72, 0, 1, 116, 0, 0, 7, 114, 72, 0, 2, 27, 73, 52, 128, 114, 76, 0, 8, 16, 77, 36, 192, 38, 76, 0, 0, 40, 29, 68, 64, 26, 80, 112, 0, 16, 73, 68, 64, 95, 73, 0, 0, 114, 64, 0, 8, 16, 65, 20, 0, 80, 71, 176, 112, 95, 237, 64, 14, 95, 237, 48, 15, 95, 237, 0, 16, 80, 67, 176, 48, 114, 72, 0, 24, 40, 65, 20, 128, 80, 71, 176, 136, 114, 72, 0, 24, 40, 69, 4, 128, 80, 67, 177, 8, 114, 72, 0, 24, 40, 65, 20, 128, 80, 71, 177, 48, 114, 72, 0, 24, 40, 69, 4, 128, 80, 67, 177, 48, 80, 71, 176, 240, 114, 72, 0, 24, 40, 69, 4, 128, 80, 67, 176, 224, 26, 233, 16, 0, 26, 229, 0, 0, 32, 248, 51, 0, 88, 251, 224, 2, 80, 251, 224, 4, 116, 0, 0, 56, 26, 67, 208, 0, 80, 71, 176, 72, 114, 72, 0, 16, 40, 69, 4, 128, 80, 67, 177, 32, 114, 72, 0, 16, 40, 65, 20, 128, 80, 71, 176, 184, 114, 72, 0, 16, 40, 69, 4, 128, 93, 67, 240, 0, 93, 71, 176, 23, 93, 75, 176, 24, 52, 1, 4, 82, 26, 244, 0, 0, 116, 0, 0, 8, 93, 67, 176, 41, 16, 65, 0, 64, 95, 237, 0, 41, 93, 67, 176, 42, 93, 71, 176, 41, 27, 65, 4, 64, 95, 237, 0, 42, 117, 0, 0, 91, 146, 0, 1, 88, 26, 249, 80, 0, 152, 8, 0, 0, 151, 0, 0, 63, 74, 248, 0, 0, 149, 0, 0, 15, 150, 8, 0, 0, 26, 236, 80, 0, 145, 0, 0, 72, 26, 67, 160, 0, 26, 71, 224, 0, 114, 72, 4, 0, 38, 72, 0, 0, 26, 72, 112, 0, 80, 79, 176, 24, 95, 237, 32, 3, 114, 72, 4, 0, 95, 237, 32, 4, 95, 236, 0, 5, 114, 72, 0, 24, 40, 237, 52, 128, 80, 75, 176, 48, 114, 76, 0, 24, 40, 75, 180, 192, 114, 76, 0, 24, 40, 65, 36, 192, 26, 245, 0, 0, 146, 0, 0, 72, 26, 249, 16, 0, 152, 8, 0, 0, 151, 0, 0, 15, 74, 248, 0, 0, 149, 0, 0, 63, 150, 8, 0, 0, 26, 236, 80, 0, 145, 0, 0, 104, 26, 67, 160, 0, 26, 71, 144, 0, 26, 75, 224, 0, 80, 79, 176, 80, 114, 80, 0, 24, 40, 77, 5, 0, 114, 64, 0, 24, 40, 237, 52, 0, 80, 67, 176, 40, 114, 76, 0, 24, 40, 67, 180, 192, 93, 79, 176, 5, 80, 65, 0, 16, 80, 83, 176, 64, 95, 237, 48, 8, 80, 77, 64, 8, 114, 84, 0, 8, 40, 77, 5, 64, 80, 67, 176, 24, 114, 76, 0, 16, 40, 65, 68, 192, 114, 76, 0, 16, 40, 69, 4, 192, 26, 245, 16, 0, 146, 0, 0, 104, 26, 249, 32, 0, 152, 8, 0, 0, 151, 0, 0, 63, 74, 248, 0, 0, 71, 0, 0, 0, 21, 6, 230, 244, 76, 29, 98, 145
827
],
838
"script_data": [],
849
"receipts_root": "0000000000000000000000000000000000000000000000000000000000000000"
8510
},
8611
"policies": {
8712
"bits": "MaxFee",
88-
"values": [
89-
0,
90-
0,
91-
0,
92-
0
93-
]
13+
"values": [0, 0, 0, 0]
9414
},
9515
"inputs": [
9616
{
@@ -117,72 +37,7 @@
11737
"outputs": [],
11838
"witnesses": [
11939
{
120-
"data": [
121-
156,
122-
254,
123-
34,
124-
102,
125-
65,
126-
96,
127-
133,
128-
170,
129-
254,
130-
105,
131-
147,
132-
35,
133-
196,
134-
199,
135-
179,
136-
133,
137-
132,
138-
240,
139-
208,
140-
149,
141-
11,
142-
46,
143-
30,
144-
96,
145-
44,
146-
91,
147-
121,
148-
195,
149-
145,
150-
184,
151-
159,
152-
235,
153-
117,
154-
82,
155-
135,
156-
41,
157-
84,
158-
154,
159-
102,
160-
61,
161-
61,
162-
16,
163-
99,
164-
123,
165-
58,
166-
173,
167-
75,
168-
226,
169-
219,
170-
139,
171-
62,
172-
33,
173-
41,
174-
176,
175-
16,
176-
18,
177-
132,
178-
178,
179-
8,
180-
125,
181-
130,
182-
169,
183-
32,
184-
108
185-
]
40+
"data": [156, 254, 34, 102, 65, 96, 133, 170, 254, 105, 147, 35, 196, 199, 179, 133, 132, 240, 208, 149, 11, 46, 30, 96, 44, 91, 121, 195, 145, 184, 159, 235, 117, 82, 135, 41, 84, 154, 102, 61, 61, 16, 99, 123, 58, 173, 75, 226, 219, 139, 62, 33, 41, 176, 16, 18, 132, 178, 8, 125, 130, 169, 32, 108]
18641
}
18742
]
18843
}

forc-plugins/forc-debug/src/cli/commands.rs

+112-10
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ use crate::{
44
names::{register_index, register_name},
55
ContractId, RunResult, Transaction,
66
};
7+
use fuel_tx::Receipt;
78
use fuel_vm::consts::{VM_MAX_RAM, VM_REGISTER_COUNT, WORD_SIZE};
89
use std::collections::HashSet;
910
use strsim::levenshtein;
11+
use sway_core::asm_generation::ProgramABI;
1012

1113
#[derive(Debug, Clone)]
1214
pub struct Command {
@@ -159,24 +161,104 @@ impl Commands {
159161
}
160162
}
161163

164+
/// Start a debugging session for a transaction with optional ABI support.
165+
///
166+
/// Handles two distinct modes of operation:
167+
/// 1. Local Development: `tx transaction.json abi.json`
168+
/// 2. Contract-specific: `tx transaction.json --abi <contract_id>:<abi_file.json>`
169+
///
170+
/// In both modes, the function will automatically attempt to fetch ABIs for any
171+
/// contract IDs encountered during execution if they haven't been explicitly provided.
172+
///
173+
/// # Arguments format
174+
/// - First argument: Path to transaction JSON file (required)
175+
/// - Local dev mode: Optional path to ABI JSON file
176+
/// - Contract mode: Multiple `--abi contract_id:abi_file.json` pairs
177+
///
178+
/// # Example usage
179+
/// ```text
180+
/// tx transaction.json // No ABI
181+
/// tx transaction.json abi.json // Local development
182+
/// tx transaction.json --abi 0x123...:contract.json // Single contract
183+
/// tx transaction.json --abi 0x123...:a.json --abi 0x456...:b.json // Multiple
184+
/// ```
162185
pub async fn cmd_start_tx(state: &mut State, mut args: Vec<String>) -> Result<()> {
163-
args.remove(0); // Remove the command name
164-
ArgumentError::ensure_arg_count(&args, 1, 1)?; // Ensure exactly one argument
186+
// Remove command name from arguments
187+
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+
// Parse arguments iteratively, handling both --abi flags and local dev mode
194+
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+
// First non-flag argument is the transaction path
208+
tx_path = Some(arg.to_string());
209+
} else if arg.ends_with(".json") {
210+
// Second .json file is treated as local development ABI
211+
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+
}
165225

166-
let path_to_tx_json = args.pop().unwrap(); // Safe due to arg count check
226+
let tx_path =
227+
tx_path.ok_or_else(|| ArgumentError::Invalid("Transaction file required".into()))?;
228+
229+
// Process contract-specific ABI mappings from --abi arguments
230+
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+
}
167250

168-
// Read and parse the transaction JSON
169-
let tx_json = std::fs::read(&path_to_tx_json).map_err(Error::IoError)?;
251+
// Start transaction execution
252+
let tx_json = std::fs::read(&tx_path).map_err(Error::IoError)?;
170253
let tx: Transaction = serde_json::from_slice(&tx_json).map_err(Error::JsonError)?;
171254

172-
// Start the transaction
173255
let status = state
174256
.client
175257
.start_tx(&state.session_id, &tx)
176258
.await
177259
.map_err(|e| Error::FuelClientError(e.to_string()))?;
178260

179-
pretty_print_run_result(&status);
261+
pretty_print_run_result(&status, state);
180262
Ok(())
181263
}
182264

@@ -205,7 +287,7 @@ pub async fn cmd_continue(state: &mut State, mut args: Vec<String>) -> Result<()
205287
.await
206288
.map_err(|e| Error::FuelClientError(e.to_string()))?;
207289

208-
pretty_print_run_result(&status);
290+
pretty_print_run_result(&status, state);
209291
Ok(())
210292
}
211293

@@ -364,9 +446,29 @@ pub async fn cmd_help(helper: &DebuggerHelper, args: &[String]) -> Result<()> {
364446
///
365447
/// Outputs each receipt in the `RunResult` and details about the breakpoint if present.
366448
/// If the execution terminated without hitting a breakpoint, it prints "Terminated".
367-
fn pretty_print_run_result(rr: &RunResult) {
449+
fn pretty_print_run_result(rr: &RunResult, state: &mut State) {
368450
for receipt in rr.receipts() {
369-
println!("Receipt: {receipt:#?}");
451+
println!("Receipt: {receipt:?}");
452+
453+
if let Receipt::LogData {
454+
id,
455+
rb,
456+
data: Some(data),
457+
..
458+
} = receipt
459+
{
460+
// If the ABI is available, decode the log data
461+
if let Some(abi) = state.contract_abis.get_or_fetch_abi(&id) {
462+
if let Ok(decoded_log_data) =
463+
forc_test::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+
}
370472
}
371473
if let Some(bp) = &rr.breakpoint {
372474
println!(

0 commit comments

Comments
 (0)