use crate::{
cli::state::{DebuggerHelper, State},
error::{ArgumentError, Error, Result},
names::{register_index, register_name},
ContractId, RunResult, Transaction,
};
use fuel_vm::consts::{VM_MAX_RAM, VM_REGISTER_COUNT, WORD_SIZE};
use std::collections::HashSet;
use strsim::levenshtein;
#[derive(Debug, Clone)]
pub struct Command {
pub name: &'static str,
pub aliases: &'static [&'static str],
pub help: &'static str,
}
pub struct Commands {
pub tx: Command,
pub reset: Command,
pub continue_: Command,
pub step: Command,
pub breakpoint: Command,
pub registers: Command,
pub memory: Command,
pub quit: Command,
pub help: Command,
}
impl Commands {
pub const fn new() -> Self {
Self {
tx: Command {
name: "start_tx",
aliases: &["n", "tx", "new_tx"],
help: "Start a new transaction",
},
reset: Command {
name: "reset",
aliases: &[],
help: "Reset debugger state",
},
continue_: Command {
name: "continue",
aliases: &["c"],
help: "Continue execution",
},
step: Command {
name: "step",
aliases: &["s"],
help: "Step execution",
},
breakpoint: Command {
name: "breakpoint",
aliases: &["b"],
help: "Set breakpoint",
},
registers: Command {
name: "register",
aliases: &["r", "reg", "registers"],
help: "View registers",
},
memory: Command {
name: "memory",
aliases: &["m", "mem"],
help: "View memory",
},
quit: Command {
name: "quit",
aliases: &["exit"],
help: "Exit debugger",
},
help: Command {
name: "help",
aliases: &["h", "?"],
help: "Show help for commands",
},
}
}
pub fn all_commands(&self) -> Vec<&Command> {
vec![
&self.tx,
&self.reset,
&self.continue_,
&self.step,
&self.breakpoint,
&self.registers,
&self.memory,
&self.quit,
&self.help,
]
}
pub fn is_tx_command(&self, cmd: &str) -> bool {
self.tx.name == cmd || self.tx.aliases.contains(&cmd)
}
pub fn is_register_command(&self, cmd: &str) -> bool {
self.registers.name == cmd || self.registers.aliases.contains(&cmd)
}
pub fn is_memory_command(&self, cmd: &str) -> bool {
self.memory.name == cmd || self.memory.aliases.contains(&cmd)
}
pub fn is_breakpoint_command(&self, cmd: &str) -> bool {
self.breakpoint.name == cmd || self.breakpoint.aliases.contains(&cmd)
}
pub fn is_quit_command(&self, cmd: &str) -> bool {
self.quit.name == cmd || self.quit.aliases.contains(&cmd)
}
pub fn is_reset_command(&self, cmd: &str) -> bool {
self.reset.name == cmd || self.reset.aliases.contains(&cmd)
}
pub fn is_continue_command(&self, cmd: &str) -> bool {
self.continue_.name == cmd || self.continue_.aliases.contains(&cmd)
}
pub fn is_step_command(&self, cmd: &str) -> bool {
self.step.name == cmd || self.step.aliases.contains(&cmd)
}
pub fn is_help_command(&self, cmd: &str) -> bool {
self.help.name == cmd || self.help.aliases.contains(&cmd)
}
pub fn find_command(&self, name: &str) -> Option<&Command> {
self.all_commands()
.into_iter()
.find(|cmd| cmd.name == name || cmd.aliases.contains(&name))
}
pub fn get_all_command_strings(&self) -> HashSet<&'static str> {
let mut commands = HashSet::new();
for cmd in self.all_commands() {
commands.insert(cmd.name);
commands.extend(cmd.aliases);
}
commands
}
pub fn find_closest(&self, unknown_cmd: &str) -> Option<&Command> {
self.all_commands()
.into_iter()
.flat_map(|cmd| {
std::iter::once((cmd, cmd.name))
.chain(cmd.aliases.iter().map(move |&alias| (cmd, alias)))
})
.map(|(cmd, name)| (cmd, levenshtein(unknown_cmd, name)))
.filter(|&(_, distance)| distance <= 2)
.min_by_key(|&(_, distance)| distance)
.map(|(cmd, _)| cmd)
}
}
pub async fn cmd_start_tx(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); ArgumentError::ensure_arg_count(&args, 1, 1)?; let path_to_tx_json = args.pop().unwrap(); let tx_json = std::fs::read(&path_to_tx_json).map_err(Error::IoError)?;
let tx: Transaction = serde_json::from_slice(&tx_json).map_err(Error::JsonError)?;
let status = state
.client
.start_tx(&state.session_id, &tx)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
pretty_print_run_result(&status);
Ok(())
}
pub async fn cmd_reset(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); ArgumentError::ensure_arg_count(&args, 0, 0)?; state
.client
.reset(&state.session_id)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
Ok(())
}
pub async fn cmd_continue(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); ArgumentError::ensure_arg_count(&args, 0, 0)?; let status = state
.client
.continue_tx(&state.session_id)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
pretty_print_run_result(&status);
Ok(())
}
pub async fn cmd_step(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); ArgumentError::ensure_arg_count(&args, 0, 1)?; let enable = args
.first()
.map_or(true, |v| !["off", "no", "disable"].contains(&v.as_str()));
state
.client
.set_single_stepping(&state.session_id, enable)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
Ok(())
}
pub async fn cmd_breakpoint(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); ArgumentError::ensure_arg_count(&args, 1, 2)?;
let offset_str = args.pop().unwrap(); let offset = parse_int(&offset_str).ok_or(ArgumentError::InvalidNumber(offset_str))?;
let contract = if let Some(contract_id) = args.pop() {
contract_id
.parse::<ContractId>()
.map_err(|_| ArgumentError::Invalid(format!("Invalid contract ID: {}", contract_id)))?
} else {
ContractId::zeroed()
};
state
.client
.set_breakpoint(&state.session_id, contract, offset as u64)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
Ok(())
}
pub async fn cmd_registers(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); if args.is_empty() {
for r in 0..VM_REGISTER_COUNT {
let value = state
.client
.register(&state.session_id, r as u32)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
println!("reg[{:#x}] = {:<8} # {}", r, value, register_name(r));
}
} else {
for arg in &args {
if let Some(v) = parse_int(arg) {
if v < VM_REGISTER_COUNT {
let value = state
.client
.register(&state.session_id, v as u32)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
println!("reg[{:#02x}] = {:<8} # {}", v, value, register_name(v));
} else {
return Err(ArgumentError::InvalidNumber(format!(
"Register index too large: {v}"
))
.into());
}
} else if let Some(index) = register_index(arg) {
let value = state
.client
.register(&state.session_id, index as u32)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
println!("reg[{index:#02x}] = {value:<8} # {arg}");
} else {
return Err(ArgumentError::Invalid(format!("Unknown register name: {arg}")).into());
}
}
}
Ok(())
}
pub async fn cmd_memory(state: &mut State, mut args: Vec<String>) -> Result<()> {
args.remove(0); let limit = args
.pop()
.map(|a| parse_int(&a).ok_or(ArgumentError::InvalidNumber(a)))
.transpose()?
.unwrap_or(WORD_SIZE * (VM_MAX_RAM as usize));
let offset = args
.pop()
.map(|a| parse_int(&a).ok_or(ArgumentError::InvalidNumber(a)))
.transpose()?
.unwrap_or(0);
ArgumentError::ensure_arg_count(&args, 0, 2)?;
let mem = state
.client
.memory(&state.session_id, offset as u32, limit as u32)
.await
.map_err(|e| Error::FuelClientError(e.to_string()))?;
for (i, chunk) in mem.chunks(WORD_SIZE).enumerate() {
print!(" {:06x}:", offset + i * WORD_SIZE);
for byte in chunk {
print!(" {byte:02x}");
}
println!();
}
Ok(())
}
pub async fn cmd_help(helper: &DebuggerHelper, args: &[String]) -> Result<()> {
if args.len() > 1 {
if let Some(cmd) = helper.commands.find_command(&args[1]) {
println!("{} - {}", cmd.name, cmd.help);
if !cmd.aliases.is_empty() {
println!("Aliases: {}", cmd.aliases.join(", "));
}
return Ok(());
}
println!("Unknown command: '{}'", args[1]);
}
println!("Available commands:");
for cmd in helper.commands.all_commands() {
println!(" {:<12} - {}", cmd.name, cmd.help);
if !cmd.aliases.is_empty() {
println!(" aliases: {}", cmd.aliases.join(", "));
}
}
Ok(())
}
fn pretty_print_run_result(rr: &RunResult) {
for receipt in rr.receipts() {
println!("Receipt: {receipt:#?}");
}
if let Some(bp) = &rr.breakpoint {
println!(
"Stopped on breakpoint at address {} of contract {}",
bp.pc.0, bp.contract
);
} else {
println!("Terminated");
}
}
pub fn parse_int(s: &str) -> Option<usize> {
let (s, radix) = s.strip_prefix("0x").map_or((s, 10), |s| (s, 16));
usize::from_str_radix(&s.replace('_', ""), radix).ok()
}