Skip to content

Commit

Permalink
forc-debug: Add ABI support for decoding log values (FuelLabs#6856)
Browse files Browse the repository at this point in the history
## 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 FuelLabs#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.
  • Loading branch information
JoshuaBatty authored and tritao committed Feb 7, 2025
1 parent 64c359f commit 78b3d56
Show file tree
Hide file tree
Showing 11 changed files with 378 additions and 436 deletions.
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion docs/book/spell-check-custom-words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -236,4 +236,5 @@ semiautomatically
FuelLabs
github
toml
hardcoded
hardcoded
subdirectories
296 changes: 64 additions & 232 deletions docs/book/src/debugging/debugging_with_cli.md

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions forc-plugins/forc-debug/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,9 @@ dirs.workspace = true
forc-pkg.workspace = true
forc-test.workspace = true
forc-tracing.workspace = true
fuel-abi-types.workspace = true
fuel-core-client.workspace = true
fuel-tx.workspace = true
fuel-types = { workspace = true, features = ["serde"] }
fuel-vm = { workspace = true, features = ["serde"] }
rayon.workspace = true
Expand Down
32 changes: 32 additions & 0 deletions forc-plugins/forc-debug/examples/example_abi.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
{
"programType": "script",
"specVersion": "1",
"encodingVersion": "1",
"concreteTypes": [
{
"type": "()",
"concreteTypeId": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d"
},
{
"type": "u64",
"concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
}
],
"metadataTypes": [],
"functions": [
{
"inputs": [],
"name": "main",
"output": "2e38e77b22c314a449e91fafed92a43826ac6aa403ae6a8acb6cf58239fbaf5d",
"attributes": null
}
],
"loggedTypes": [
{
"logId": "1515152261580153489",
"concreteTypeId": "1506e6f44c1d6291cdf46395a8e573276a4fa79e8ace3fc891e092ef32d1b0a0"
}
],
"messagesTypes": [],
"configurables": []
}
151 changes: 3 additions & 148 deletions forc-plugins/forc-debug/examples/example_tx.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,94 +3,14 @@
"body": {
"script_gas_limit": 1000000,
"script": [
144,
0,
0,
4,
71,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
68,
93,
252,
192,
1,
16,
255,
243,
0,
26,
72,
16,
0,
26,
68,
0,
0,
93,
67,
240,
0,
22,
65,
20,
0,
115,
64,
0,
13,
51,
72,
0,
0,
36,
0,
0,
0,
16,
69,
16,
64,
27,
73,
36,
64,
144,
0,
0,
8,
71,
0,
0,
0,
0,
0,
0,
0,
0,
0,
0,
5
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
],
"script_data": [],
"receipts_root": "0000000000000000000000000000000000000000000000000000000000000000"
},
"policies": {
"bits": "MaxFee",
"values": [
0,
0,
0,
0
]
"values": [0, 0, 0, 0]
},
"inputs": [
{
Expand All @@ -117,72 +37,7 @@
"outputs": [],
"witnesses": [
{
"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
]
"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]
}
]
}
Expand Down
122 changes: 112 additions & 10 deletions forc-plugins/forc-debug/src/cli/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@ use crate::{
names::{register_index, register_name},
ContractId, RunResult, Transaction,
};
use fuel_tx::Receipt;
use fuel_vm::consts::{VM_MAX_RAM, VM_REGISTER_COUNT, WORD_SIZE};
use std::collections::HashSet;
use strsim::levenshtein;
use sway_core::asm_generation::ProgramABI;

#[derive(Debug, Clone)]
pub struct Command {
Expand Down Expand Up @@ -159,24 +161,104 @@ impl Commands {
}
}

/// Start a debugging session for a transaction with optional ABI support.
///
/// Handles two distinct modes of operation:
/// 1. Local Development: `tx transaction.json abi.json`
/// 2. Contract-specific: `tx transaction.json --abi <contract_id>:<abi_file.json>`
///
/// In both modes, the function will automatically attempt to fetch ABIs for any
/// contract IDs encountered during execution if they haven't been explicitly provided.
///
/// # Arguments format
/// - First argument: Path to transaction JSON file (required)
/// - Local dev mode: Optional path to ABI JSON file
/// - Contract mode: Multiple `--abi contract_id:abi_file.json` pairs
///
/// # Example usage
/// ```text
/// tx transaction.json // No ABI
/// tx transaction.json abi.json // Local development
/// tx transaction.json --abi 0x123...:contract.json // Single contract
/// tx transaction.json --abi 0x123...:a.json --abi 0x456...:b.json // Multiple
/// ```
pub async fn cmd_start_tx(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); // Remove the command name
ArgumentError::ensure_arg_count(&args, 1, 1)?; // Ensure exactly one argument
// Remove command name from arguments
args.remove(0);
ArgumentError::ensure_arg_count(&args, 1, 2)?;

let mut abi_args = Vec::new();
let mut tx_path = None;

// Parse arguments iteratively, handling both --abi flags and local dev mode
let mut i = 0;
while i < args.len() {
match args[i].as_str() {
"--abi" => {
if i + 1 < args.len() {
abi_args.push(args[i + 1].clone());
i += 2;
} else {
return Err(ArgumentError::Invalid("Missing argument for --abi".into()).into());
}
}
arg => {
if tx_path.is_none() {
// First non-flag argument is the transaction path
tx_path = Some(arg.to_string());
} else if arg.ends_with(".json") {
// Second .json file is treated as local development ABI
let abi_content = std::fs::read_to_string(arg).map_err(Error::IoError)?;
let fuel_abi =
serde_json::from_str::<fuel_abi_types::abi::program::ProgramABI>(
&abi_content,
)
.map_err(Error::JsonError)?;
state
.contract_abis
.register_abi(ContractId::zeroed(), ProgramABI::Fuel(fuel_abi));
}
i += 1;
}
}
}

let path_to_tx_json = args.pop().unwrap(); // Safe due to arg count check
let tx_path =
tx_path.ok_or_else(|| ArgumentError::Invalid("Transaction file required".into()))?;

// Process contract-specific ABI mappings from --abi arguments
for abi_arg in abi_args {
if let Some((contract_id, abi_path)) = abi_arg.split_once(':') {
let contract_id = contract_id.parse::<ContractId>().map_err(|_| {
ArgumentError::Invalid(format!("Invalid contract ID: {}", contract_id))
})?;

let abi_content = std::fs::read_to_string(abi_path).map_err(Error::IoError)?;
let fuel_abi =
serde_json::from_str::<fuel_abi_types::abi::program::ProgramABI>(&abi_content)
.map_err(Error::JsonError)?;

state
.contract_abis
.register_abi(contract_id, ProgramABI::Fuel(fuel_abi));
} else {
return Err(
ArgumentError::Invalid(format!("Invalid --abi argument: {}", abi_arg)).into(),
);
}
}

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

// Start the transaction
let status = state
.client
.start_tx(&state.session_id, &tx)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;

pretty_print_run_result(&status);
pretty_print_run_result(&status, state);
Ok(())
}

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

pretty_print_run_result(&status);
pretty_print_run_result(&status, state);
Ok(())
}

Expand Down Expand Up @@ -364,9 +446,29 @@ pub async fn cmd_help(helper: &DebuggerHelper, args: &[String]) -> Result<()> {
///
/// Outputs each receipt in the `RunResult` and details about the breakpoint if present.
/// If the execution terminated without hitting a breakpoint, it prints "Terminated".
fn pretty_print_run_result(rr: &RunResult) {
fn pretty_print_run_result(rr: &RunResult, state: &mut State) {
for receipt in rr.receipts() {
println!("Receipt: {receipt:#?}");
println!("Receipt: {receipt:?}");

if let Receipt::LogData {
id,
rb,
data: Some(data),
..
} = receipt
{
// If the ABI is available, decode the log data
if let Some(abi) = state.contract_abis.get_or_fetch_abi(&id) {
if let Ok(decoded_log_data) =
forc_test::decode_log_data(&rb.to_string(), &data, abi)
{
println!(
"Decoded log value: {}, from contract: {}",
decoded_log_data.value, id
);
}
}
}
}
if let Some(bp) = &rr.breakpoint {
println!(
Expand Down
Loading

0 comments on commit 78b3d56

Please sign in to comment.