pub mod storage;
use crate::{cmd, op::call::Abi};
use ansiterm::Color;
use anyhow::{anyhow, Result};
use fuel_abi_types::{
abi::program::PanickingCall,
revert_info::{RevertInfo, RevertKind},
};
use fuel_core_types::tai64::Tai64;
use fuel_tx::Receipt;
use fuel_vm::{
fuel_asm::Word,
fuel_types::BlockHeight,
interpreter::{Interpreter, InterpreterParams, MemoryInstance},
prelude::*,
state::ProgramState,
};
use fuels::types::Token;
use fuels_core::{
codec::{ABIDecoder, DecoderConfig},
types::{param_types::ParamType, ContractId},
};
use std::{collections::HashMap, io::Read};
use storage::ShallowStorage;
#[derive(Clone)]
pub struct MemoryReader<'a> {
mem: &'a MemoryInstance,
at: Word,
}
impl<'a> MemoryReader<'a> {
pub fn new(mem: &'a MemoryInstance, at: Word) -> Self {
Self { mem, at }
}
}
impl Read for MemoryReader<'_> {
fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
let at = self.at;
self.at += buf.len() as Word;
buf.copy_from_slice(
self.mem
.read(at, buf.len())
.map_err(|_err| std::io::Error::other("Inaccessible memory"))?,
);
Ok(buf.len())
}
}
pub async fn interpret_execution_trace(
provider: &fuels::accounts::provider::Provider,
mode: &cmd::call::ExecutionMode,
consensus_params: &ConsensusParameters,
script: &fuel_tx::Script,
receipts: &[Receipt],
storage_reads: Vec<fuel_core_types::services::executor::StorageReadReplayEvent>,
abis: &HashMap<ContractId, Abi>,
) -> Result<Vec<TraceEvent>> {
let mut tracer = CallRetTracer::new(abis);
let block_height: BlockHeight = (provider.latest_block_height().await?).into();
let gas_price = provider.latest_gas_price().await?;
let block = provider
.block_by_height(block_height)
.await?
.ok_or(anyhow!("Block not found"))?;
let storage = ShallowStorage {
block_height,
timestamp: Tai64::from_unix(
block
.header
.time
.ok_or(anyhow!("Block time not found"))?
.timestamp(),
),
consensus_parameters_version: block.header.consensus_parameters_version,
state_transition_version: block.header.state_transition_bytecode_version,
coinbase: Default::default(), storage: std::cell::RefCell::new(ShallowStorage::initial_storage(storage_reads)),
};
let script_tx = script
.clone()
.into_checked_basic(block_height, consensus_params)
.map_err(|err| anyhow!("Failed to check transaction: {err:?}"))?
.into_ready(
gas_price.gas_price,
consensus_params.gas_costs(),
consensus_params.fee_params(),
None,
)
.map_err(|err| anyhow!("Failed to check transaction: {err:?}"))?;
let mut vm = Interpreter::<_, _, Script>::with_storage(
MemoryInstance::new(),
storage.clone(),
InterpreterParams::new(gas_price.gas_price, consensus_params),
);
vm.set_single_stepping(true);
let mut t = *vm
.transact(script_tx)
.map_err(|e| anyhow!("Failed to transact in trace interpreter: {e:?}"))?
.state();
loop {
tracer.process_vm_state(&vm)?;
match t {
ProgramState::Return(_) | ProgramState::ReturnData(_) | ProgramState::Revert(_) => {
break
}
ProgramState::RunProgram(_) | ProgramState::VerifyPredicate(_) => {
t = vm
.resume()
.map_err(|e| anyhow!("Failed to resume VM in trace interpreter: {e:?}"))?;
}
}
}
if vm.receipts() != receipts {
match mode {
cmd::call::ExecutionMode::Live => return Err(anyhow!("Receipts mismatch")),
_ => forc_tracing::println_warning(
"Receipts mismatch; this is expected for non-live mode",
),
}
}
Ok(tracer.into_events())
}
#[allow(dead_code)]
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub enum TraceEvent {
Call {
index: usize,
method: String,
arguments: Option<Vec<String>>,
to: ContractId,
amount: u64,
gas: u64,
},
Return {
index: usize,
id: ContractId,
val: u64,
},
ReturnData {
index: usize,
id: ContractId,
data: String,
},
Panic {
index: usize,
id: ContractId,
reason: String,
contract_id: Option<ContractId>,
},
Revert {
index: usize,
id: ContractId,
ra: u64,
revert_info: Option<RevertInfoSummary>,
},
Log {
index: usize,
id: ContractId,
ra: u64,
rb: u64,
rc: u64,
rd: u64,
},
LogData {
index: usize,
id: ContractId,
value: Option<String>,
len: u64,
},
Transfer {
index: usize,
id: ContractId,
to: String,
amount: u64,
asset_id: String,
},
ScriptResult {
index: usize,
result: ScriptExecutionResult,
gas_used: u64,
},
MessageOut {
index: usize,
sender: String,
recipient: String,
nonce: u64,
digest: String,
amount: u64,
data: Option<String>,
},
Mint {
index: usize,
contract_id: ContractId,
asset_id: String,
val: u64,
},
Burn {
index: usize,
contract_id: ContractId,
asset_id: String,
val: u64,
},
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct PanicLocation {
pub function: String,
pub pkg: String,
pub file: String,
pub line: u64,
pub column: u64,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RevertInfoSummary {
pub revert_code: u64,
pub message: Option<String>,
pub value: Option<String>,
pub location: Option<PanicLocation>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub backtrace: Vec<PanickingCall>,
pub is_known_error: bool,
pub is_raw: bool,
}
impl From<RevertInfo> for RevertInfoSummary {
fn from(info: RevertInfo) -> Self {
match info.kind {
RevertKind::RawRevert => Self {
revert_code: info.revert_code,
message: None,
value: None,
location: None,
backtrace: vec![],
is_known_error: false,
is_raw: true,
},
RevertKind::KnownErrorSignal { err_msg } => Self {
revert_code: info.revert_code,
message: Some(err_msg),
value: None,
location: None,
backtrace: vec![],
is_known_error: true,
is_raw: false,
},
RevertKind::Panic {
err_msg,
err_val,
pos,
backtrace,
} => Self {
revert_code: info.revert_code,
message: err_msg,
value: err_val,
location: Some(PanicLocation {
function: pos.function,
pkg: pos.pkg,
file: pos.file,
line: pos.line,
column: pos.column,
}),
backtrace,
is_known_error: false,
is_raw: false,
},
}
}
}
pub fn first_revert_info(events: &[TraceEvent]) -> Option<(ContractId, RevertInfoSummary)> {
events.iter().find_map(|e| {
if let TraceEvent::Revert {
id,
revert_info: Some(info),
..
} = e
{
Some((*id, info.clone()))
} else {
None
}
})
}
fn decode_revert_info(
receipts: &[Receipt],
abis: &HashMap<ContractId, Abi>,
contract_id: ContractId,
revert_code: u64,
) -> Option<RevertInfoSummary> {
let program_abi = abis.get(&contract_id).map(|abi| &abi.program);
let info =
forc_util::tx_utils::revert_info_from_receipts(receipts, program_abi, Some(revert_code))?;
if info.revert_code != revert_code {
return None;
}
Some(RevertInfoSummary::from(info))
}
pub fn display_transaction_trace<W: std::io::Write>(
total_gas: u64,
trace_events: &[TraceEvent],
labels: &HashMap<ContractId, String>,
writer: &mut W,
) -> Result<()> {
let format_contract_with_label =
|contract_id: ContractId, labels: &HashMap<ContractId, String>| -> String {
if let Some(label) = labels.get(&contract_id) {
label.to_string()
} else {
format!("0x{contract_id}")
}
};
writeln!(writer, "Traces:")?;
writeln!(writer, " [Script]")?;
let mut depth = 0;
for event in trace_events {
let indent = if depth > 0 {
" │".repeat(depth)
} else {
"".to_string()
};
match event {
TraceEvent::Call {
to,
gas,
method,
arguments,
..
} => {
writeln!(
writer,
"{} ├─ [{}] {}{}{}({})",
indent,
gas,
Color::Green.paint(format_contract_with_label(*to, labels)),
Color::DarkGray.paint("::"),
method,
Color::DarkGray.paint(arguments.as_ref().unwrap_or(&vec![]).join(", "))
)?;
depth += 1;
}
TraceEvent::ReturnData { data, .. } => {
writeln!(
writer,
"{} └─ ← {}",
indent,
Color::BrightCyan.paint(data),
)?;
depth = depth.saturating_sub(1);
}
TraceEvent::Return { val, .. } => {
writeln!(writer, "{indent} └─ ← [Return] val: {val}")?;
depth = depth.saturating_sub(1);
}
TraceEvent::LogData { value, .. } => {
if let Some(log_value) = value {
writeln!(
writer,
"{} ├─ emit {}",
indent,
Color::BrightCyan.paint(log_value)
)?;
} else {
writeln!(writer, "{indent} ├─ emit ()")?;
}
}
TraceEvent::Revert { revert_info, .. } => {
writeln!(
writer,
"{} └─ ← {}",
indent,
Color::Red.paint("[Revert]")
)?;
depth = depth.saturating_sub(1);
if let Some(details) = revert_info {
write_revert_trace_details(writer, &indent, details)?;
}
}
TraceEvent::Panic { reason, .. } => {
writeln!(
writer,
"{} └─ ← {} {}",
indent,
Color::Red.paint("[Panic]"),
Color::Red.paint(reason)
)?;
depth = depth.saturating_sub(1);
}
TraceEvent::Transfer {
amount,
asset_id,
to,
..
} => {
writeln!(
writer,
"{indent} ├─ [Transfer] to:{to} asset_id:{asset_id} amount:{amount}"
)?;
}
TraceEvent::Mint { asset_id, val, .. } => {
writeln!(
writer,
"{indent} ├─ [Mint] asset_id:{asset_id} val:{val}"
)?;
}
TraceEvent::Burn { asset_id, val, .. } => {
writeln!(
writer,
"{indent} ├─ [Burn] asset_id:{asset_id} val:{val}"
)?;
}
TraceEvent::Log { rb, .. } => {
writeln!(writer, "{indent} ├─ [Log] rb: 0x{rb:x}")?;
}
TraceEvent::MessageOut {
amount,
recipient,
nonce,
digest,
data,
..
} => {
writeln!(
writer,
"{} ├─ [MessageOut] recipient:{} amount:{} nonce:{} digest:{} data:{}",
indent,
recipient,
amount,
nonce,
digest,
data.clone().unwrap_or("()".to_string())
)?;
}
TraceEvent::ScriptResult {
result, gas_used, ..
} => {
writeln!(
writer,
" [ScriptResult] result: {result:?}, gas_used: {gas_used}"
)?;
writeln!(writer)?;
match result {
ScriptExecutionResult::Success => writeln!(
writer,
"{}",
Color::Green.paint("Transaction successfully executed.")
)?,
_ => writeln!(writer, "{}", Color::Red.paint("Transaction failed."))?,
}
}
}
}
writeln!(writer, "Gas used: {total_gas}")?;
Ok(())
}
fn write_revert_trace_details<W: std::io::Write>(
writer: &mut W,
indent: &str,
details: &RevertInfoSummary,
) -> Result<()> {
let detail_prefix = format!("{indent} ");
let write_detail_line = |writer: &mut W, symbol: &str, text: String| -> Result<()> {
writeln!(
writer,
"{}{}",
detail_prefix,
Color::Red.paint(format!("{symbol} {text}"))
)
.map_err(Into::into)
};
let has_more_details = !details.is_raw;
write_detail_line(
writer,
if has_more_details { "├─" } else { "└─" },
format!("revert code: {:x}", details.revert_code),
)?;
if details.is_known_error {
if let Some(msg) = &details.message {
write_detail_line(writer, "└─", format!("error message: {msg}"))?;
}
} else if !details.is_raw {
if let Some(err_msg) = &details.message {
write_detail_line(writer, "├─", format!("panic message: {err_msg}"))?;
}
if let Some(err_val) = &details.value {
write_detail_line(writer, "├─", format!("panic value: {err_val}"))?;
}
if let Some(location) = &details.location {
let filtered_backtrace: Vec<_> = details
.backtrace
.iter()
.filter(|call| {
!call.pos.function.ends_with("::__entry") && call.pos.function != "__entry"
})
.collect();
let branch_symbol = if filtered_backtrace.is_empty() {
"└─"
} else {
"├─"
};
let location_prefix = if filtered_backtrace.is_empty() {
format!("{indent} ")
} else {
format!("{indent} {} ", Color::Red.paint("│"))
};
write_detail_line(
writer,
branch_symbol,
format!("panicked: in {}", location.function),
)?;
let loc_line = Color::Red.paint(format!(
" └─ at {}, {}:{}:{}",
location.pkg, location.file, location.line, location.column
));
writeln!(writer, "{}{}", location_prefix, loc_line)?;
if let Some((first, rest)) = filtered_backtrace.split_first() {
let line_prefix = format!("{indent} ");
write_backtrace_call(writer, &line_prefix, first, true)?;
for call in rest {
write_backtrace_call(writer, &line_prefix, call, false)?;
}
}
}
}
Ok(())
}
fn write_backtrace_call<W: std::io::Write>(
writer: &mut W,
indent_detail: &str,
call: &PanickingCall,
is_first: bool,
) -> Result<()> {
let header_prefix = format!("{indent_detail}{}", Color::Red.paint("└─ backtrace: "));
if is_first {
writeln!(
writer,
"{header_prefix}{}",
Color::Red.paint(format!("called in {}", call.pos.function))
)?;
writeln!(
writer,
"{indent_detail} {}",
Color::Red.paint(format!(
"└─ at {}, {}:{}:{}",
call.pos.pkg, call.pos.file, call.pos.line, call.pos.column
))
)?;
} else {
writeln!(
writer,
"{indent_detail} {}",
Color::Red.paint(format!("called in {}", call.pos.function))
)?;
writeln!(
writer,
"{indent_detail} {}",
Color::Red.paint(format!(
"└─ at {}, {}:{}:{}",
call.pos.pkg, call.pos.file, call.pos.line, call.pos.column
))
)?;
};
Ok(())
}
pub type Vm =
Interpreter<MemoryInstance, ShallowStorage, Script, fuel_vm::interpreter::NotSupportedEcal>;
pub struct CallRetTracer<'a> {
abis: &'a HashMap<ContractId, Abi>,
return_type_callstack: Vec<StackFrame>,
events: Vec<TraceEvent>,
}
enum StackFrame {
KnownAbi(ParamType),
UnknownAbi,
}
impl<'a> CallRetTracer<'a> {
pub fn new(abis: &'a HashMap<ContractId, Abi>) -> Self {
Self {
abis,
return_type_callstack: Vec::new(),
events: Vec::new(),
}
}
pub fn process_vm_state(&mut self, vm: &Vm) -> Result<()> {
let start_index = self.events.len();
let decoder = ABIDecoder::new(DecoderConfig::default());
for (i, receipt) in vm.receipts().iter().enumerate().skip(start_index) {
let index = i + start_index;
let event = match receipt {
Receipt::Call {
to,
param1,
param2,
amount,
gas,
..
} => {
let method = match decoder
.decode(&ParamType::String, MemoryReader::new(vm.memory(), *param1))
{
Ok(Token::String(method)) => Some(method),
_ => None,
};
let arguments = if let Some((parameters, returns)) = method
.as_ref()
.and_then(|m| get_function_signature(self.abis.get(to)?, m.as_str()))
{
self.return_type_callstack
.push(StackFrame::KnownAbi(returns));
let args_reader = MemoryReader::new(vm.memory(), *param2);
decoder
.decode_multiple_as_debug_str(parameters.as_slice(), args_reader)
.ok()
} else {
self.return_type_callstack.push(StackFrame::UnknownAbi);
None
};
TraceEvent::Call {
index,
method: method.unwrap_or("unknown".to_string()),
arguments,
to: *to,
amount: *amount,
gas: *gas,
}
}
Receipt::Return { id, val, .. } => {
if !self.return_type_callstack.is_empty() {
let _ = self.return_type_callstack.pop().unwrap();
}
TraceEvent::Return {
index,
id: *id,
val: *val,
}
}
Receipt::ReturnData { id, ptr, data, .. } => {
let return_value = match self.return_type_callstack.pop() {
Some(StackFrame::KnownAbi(return_type)) => {
let reader = MemoryReader::new(vm.memory(), *ptr);
decoder
.decode_as_debug_str(&return_type, reader)
.unwrap_or_else(|_| match data {
Some(data) if !data.is_empty() => {
format!("0x{}", hex::encode(data))
}
_ => "()".to_string(),
})
}
Some(StackFrame::UnknownAbi) | None => match data {
Some(data) if !data.is_empty() => format!("0x{}", hex::encode(data)),
_ => "()".to_string(),
},
};
TraceEvent::ReturnData {
index,
data: return_value,
id: *id,
}
}
Receipt::Panic {
id,
reason,
contract_id,
..
} => TraceEvent::Panic {
index,
id: *id,
reason: format!("{:?}", reason.reason()),
contract_id: *contract_id,
},
Receipt::Revert { id, ra, .. } => TraceEvent::Revert {
index,
id: *id,
ra: *ra,
revert_info: decode_revert_info(vm.receipts(), self.abis, *id, *ra),
},
Receipt::Log {
id, ra, rb, rc, rd, ..
} => TraceEvent::Log {
index,
id: *id,
ra: *ra,
rb: *rb,
rc: *rc,
rd: *rd,
},
Receipt::LogData {
id, rb, len, data, ..
} => {
let data_str = match data {
Some(data) => {
let hex_str = format!("0x{}", hex::encode(data));
match self.abis.get(id) {
Some(abi) => {
let program_abi = sway_core::asm_generation::ProgramABI::Fuel(
abi.program.clone(),
);
forc_util::tx_utils::decode_log_data(
&rb.to_string(),
data,
&program_abi,
)
.ok()
.map(|decoded| decoded.value)
}
None => Some(hex_str),
}
}
None => None,
};
TraceEvent::LogData {
index,
value: data_str,
id: *id,
len: *len,
}
}
Receipt::Transfer {
id,
to,
amount,
asset_id,
..
} => TraceEvent::Transfer {
index,
id: *id,
to: format!("0x{to}"),
amount: *amount,
asset_id: format!("0x{asset_id}"),
},
Receipt::TransferOut {
id,
to,
amount,
asset_id,
..
} => TraceEvent::Transfer {
index,
id: *id,
to: format!("0x{to}"),
amount: *amount,
asset_id: format!("0x{asset_id}"),
},
Receipt::ScriptResult { result, gas_used } => TraceEvent::ScriptResult {
index,
result: *result,
gas_used: *gas_used,
},
Receipt::MessageOut {
sender,
recipient,
amount,
data,
..
} => {
let data_hex = data.as_ref().map(|d| format!("0x{}", hex::encode(d)));
TraceEvent::MessageOut {
index,
sender: format!("0x{sender}"),
recipient: format!("0x{recipient}"),
amount: *amount,
data: data_hex,
nonce: 0,
digest:
"0x0000000000000000000000000000000000000000000000000000000000000000"
.to_string(),
}
}
Receipt::Mint {
contract_id,
sub_id,
val,
..
} => TraceEvent::Mint {
index,
contract_id: *contract_id,
asset_id: format!("0x{sub_id}"),
val: *val,
},
Receipt::Burn {
contract_id,
sub_id,
val,
..
} => TraceEvent::Burn {
index,
contract_id: *contract_id,
asset_id: format!("0x{sub_id}"),
val: *val,
},
};
self.events.push(event);
}
Ok(())
}
pub fn into_events(self) -> Vec<TraceEvent> {
self.events
}
}
fn get_function_signature(abi: &Abi, method: &str) -> Option<(Vec<ParamType>, ParamType)> {
let func = abi.unified.functions.iter().find(|f| f.name == *method)?;
let mut parameters = Vec::new();
for param in &func.inputs {
parameters.push(ParamType::try_from_type_application(param, &abi.type_lookup).ok()?);
}
let returns = ParamType::try_from_type_application(&func.output, &abi.type_lookup).ok()?;
Some((parameters, returns))
}
#[cfg(test)]
mod tests {
use super::*;
use fuel_tx::ScriptExecutionResult;
use fuels_core::types::ContractId;
use std::str::FromStr;
fn normalize(s: &str) -> String {
let re = regex::Regex::new(r"\x1b\[[0-9;]*m").unwrap();
let s = re.replace_all(s, "");
s.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[test]
fn writes_revert_details_without_backtrace() {
let summary = RevertInfoSummary {
revert_code: 0xbeef,
message: Some("boom".to_string()),
value: Some("Value".to_string()),
location: Some(PanicLocation {
function: "ctx::fn".to_string(),
pkg: "pkg".to_string(),
file: "file.sw".to_string(),
line: 1,
column: 1,
}),
backtrace: vec![],
is_known_error: false,
is_raw: false,
};
let mut buf = Vec::new();
write_revert_trace_details(&mut buf, "", &summary).unwrap();
let out = normalize(&String::from_utf8(buf).unwrap());
let expected = normalize(
r#"
├─ revert code: beef
├─ panic message: boom
├─ panic value: Value
└─ panicked: in ctx::fn
└─ at pkg, file.sw:1:1
"#,
);
assert_eq!(out, expected);
}
#[test]
fn writes_revert_details_with_backtrace() {
let summary = RevertInfoSummary {
revert_code: 0xdead,
message: None,
value: None,
location: Some(PanicLocation {
function: "ctx::fn".to_string(),
pkg: "pkg".to_string(),
file: "file.sw".to_string(),
line: 1,
column: 1,
}),
backtrace: vec![
PanickingCall {
pos: fuel_abi_types::abi::program::ErrorPosition {
pkg: "pkg".to_string(),
file: "file.sw".to_string(),
line: 2,
column: 3,
function: "caller_fn".to_string(),
},
function: "caller_fn".to_string(),
},
PanickingCall {
pos: fuel_abi_types::abi::program::ErrorPosition {
pkg: "pkg".to_string(),
file: "file.sw".to_string(),
line: 4,
column: 5,
function: "root_fn".to_string(),
},
function: "root_fn".to_string(),
},
],
is_known_error: false,
is_raw: false,
};
let mut buf = Vec::new();
write_revert_trace_details(&mut buf, "│ ", &summary).unwrap();
let out = normalize(&String::from_utf8(buf).unwrap());
let expected = normalize(
r#"
│ ├─ revert code: dead
│ ├─ panicked: in ctx::fn
│ │ └─ at pkg, file.sw:1:1
│ └─ backtrace: called in caller_fn
│ └─ at pkg, file.sw:2:3
│ called in root_fn
│ └─ at pkg, file.sw:4:5
"#,
);
assert_eq!(out, expected);
}
#[test]
fn test_display_transaction_trace_revert() {
let contract1_id = ContractId::from_str(
"4211b7b7a0c3104e6b9450b7a9e1b7f61912c57c3b319a956d5d7f95b480eb8e",
)
.unwrap();
let contract2_id = ContractId::from_str(
"f6035b8ac5ad76c228784d03fbba08545820715e811f574ff77300eab5e1aee9",
)
.unwrap();
let trace_events = vec![
TraceEvent::Call {
index: 0,
method: "unknown".to_string(),
arguments: None,
to: contract1_id,
amount: 0,
gas: 46590,
},
TraceEvent::Call {
index: 1,
method: "unknown".to_string(),
arguments: None,
to: contract2_id,
amount: 0,
gas: 34124,
},
TraceEvent::LogData {
index: 2,
id: contract2_id,
value: Some("0x0000000000000001".to_string()),
len: 8,
},
TraceEvent::Revert {
index: 3,
id: contract2_id,
ra: 0,
revert_info: None,
},
TraceEvent::ScriptResult {
index: 4,
result: ScriptExecutionResult::Revert,
gas_used: 37531,
},
];
let mut output = Vec::new();
display_transaction_trace(0, &trace_events, &HashMap::new(), &mut output).unwrap();
let trace_output = String::from_utf8(output).unwrap();
let expected_output = r#"
Traces:
[Script]
├─ [46590] 0x4211b7b7a0c3104e6b9450b7a9e1b7f61912c57c3b319a956d5d7f95b480eb8e::unknown()
│ ├─ [34124] 0xf6035b8ac5ad76c228784d03fbba08545820715e811f574ff77300eab5e1aee9::unknown()
│ │ ├─ emit 0x0000000000000001
│ │ └─ ← [Revert]
[ScriptResult] result: Revert, gas_used: 37531
Transaction failed.
Gas used: 0
"#;
assert_eq!(
normalize(&trace_output),
normalize(expected_output),
"\nExpected:\n{expected_output}\n\nActual:\n{trace_output}\n"
);
}
#[test]
fn test_display_transaction_trace_simple_call() {
let contract_id = ContractId::from_str(
"2af09151f8276611ba65f14650970657bc42c1503d6502ffbb4d085ec37065dd",
)
.unwrap();
let trace_events = vec![
TraceEvent::Call {
index: 0,
method: "unknown".to_string(),
arguments: None,
to: contract_id,
amount: 0,
gas: 8793,
},
TraceEvent::ReturnData {
index: 1,
id: contract_id,
data: "0x00000000000000000000000000000001".to_string(),
},
TraceEvent::Return {
index: 2,
id: ContractId::zeroed(),
val: 1,
},
TraceEvent::ScriptResult {
index: 3,
result: ScriptExecutionResult::Success,
gas_used: 12400,
},
];
let mut output = Vec::new();
display_transaction_trace(0, &trace_events, &HashMap::new(), &mut output).unwrap();
let trace_output = String::from_utf8(output).unwrap();
let expected_output = r#"
Traces:
[Script]
├─ [8793] 0x2af09151f8276611ba65f14650970657bc42c1503d6502ffbb4d085ec37065dd::unknown()
│ └─ ← 0x00000000000000000000000000000001
└─ ← [Return] val: 1
[ScriptResult] result: Success, gas_used: 12400
Transaction successfully executed.
Gas used: 0
"#;
assert_eq!(
normalize(&trace_output),
normalize(expected_output),
"\nExpected:\n{expected_output}\n\nActual:\n{trace_output}\n"
);
}
#[test]
fn test_display_transaction_trace_simple_call_log() {
let contract_id = ContractId::from_str(
"4a89a8fb150bf814a6610e1172baef6c68e4e273fce379fa9b30c75f584a697e",
)
.unwrap();
let trace_events = vec![
TraceEvent::Call {
index: 0,
method: "unknown".to_string(),
arguments: None,
to: contract_id,
amount: 0,
gas: 28311,
},
TraceEvent::LogData {
index: 1,
id: contract_id,
value: Some("0x00000000000000000000000000000001".to_string()),
len: 8,
},
TraceEvent::ReturnData {
index: 2,
id: contract_id,
data: "()".to_string(),
},
TraceEvent::Return {
index: 3,
id: ContractId::zeroed(),
val: 1,
},
TraceEvent::ScriptResult {
index: 4,
result: ScriptExecutionResult::Success,
gas_used: 25412,
},
];
let mut output = Vec::new();
display_transaction_trace(0, &trace_events, &HashMap::new(), &mut output).unwrap();
let trace_output = String::from_utf8(output).unwrap();
let expected_output = r#"
Traces:
[Script]
├─ [28311] 0x4a89a8fb150bf814a6610e1172baef6c68e4e273fce379fa9b30c75f584a697e::unknown()
│ ├─ emit 0x00000000000000000000000000000001
│ └─ ← ()
└─ ← [Return] val: 1
[ScriptResult] result: Success, gas_used: 25412
Transaction successfully executed.
Gas used: 0
"#;
assert_eq!(
normalize(&trace_output),
normalize(expected_output),
"\nExpected:\n{expected_output}\n\nActual:\n{trace_output}\n"
);
}
#[test]
fn test_display_transaction_trace_call_mint_transfer_burn() {
let contract_id = ContractId::from_str(
"5598f77f568631ad7e37e1d88b248d0c5002705ae4582fd544c9a87662a6af03",
)
.unwrap();
let trace_events = vec![
TraceEvent::Call {
index: 0,
method: "unknown".to_string(),
arguments: None,
to: contract_id,
amount: 100,
gas: 46023,
},
TraceEvent::Mint {
index: 1,
contract_id,
asset_id: "0000000000000000000000000000000000000000000000000000000000000000"
.to_string(),
val: 100,
},
TraceEvent::Transfer {
index: 2,
id: contract_id,
to: "de97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c".to_string(),
amount: 100,
asset_id: "f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07"
.to_string(),
},
TraceEvent::Burn {
index: 3,
contract_id,
asset_id: "0000000000000000000000000000000000000000000000000000000000000000"
.to_string(),
val: 100,
},
TraceEvent::ReturnData {
index: 4,
id: contract_id,
data: "()".to_string(),
},
TraceEvent::Return {
index: 5,
id: ContractId::zeroed(),
val: 1,
},
TraceEvent::ScriptResult {
index: 6,
result: ScriptExecutionResult::Success,
gas_used: 37228,
},
];
let mut output = Vec::new();
display_transaction_trace(0, &trace_events, &HashMap::new(), &mut output).unwrap();
let trace_output = String::from_utf8(output).unwrap();
let expected_output = r#"
Traces:
[Script]
├─ [46023] 0x5598f77f568631ad7e37e1d88b248d0c5002705ae4582fd544c9a87662a6af03::unknown()
│ ├─ [Mint] asset_id:0000000000000000000000000000000000000000000000000000000000000000 val:100
│ ├─ [Transfer] to:de97d8624a438121b86a1956544bd72ed68cd69f2c99555b08b1e8c51ffd511c asset_id:f8f8b6283d7fa5b672b530cbb84fcccb4ff8dc40f8176ef4544ddb1f1952ad07 amount:100
│ ├─ [Burn] asset_id:0000000000000000000000000000000000000000000000000000000000000000 val:100
│ └─ ← ()
└─ ← [Return] val: 1
[ScriptResult] result: Success, gas_used: 37228
Transaction successfully executed.
Gas used: 0
"#;
assert_eq!(
normalize(&trace_output),
normalize(expected_output),
"\nExpected:\n{expected_output}\n\nActual:\n{trace_output}\n"
);
}
#[test]
fn test_display_transaction_trace_nested_call_log_success() {
let contract1_id = ContractId::from_str(
"7c05fa2efa56c4bba646af0c48db02cba34e54149785cc692ae7e297f031b12e",
)
.unwrap();
let contract2_id = ContractId::from_str(
"7ecdf7b507b33131cac7295af2156fd98cd299c3512ec8c2733f920d5f8e4506",
)
.unwrap();
let trace_events = vec![
TraceEvent::Call {
index: 0,
method: "unknown".to_string(),
arguments: None,
to: contract1_id,
amount: 0,
gas: 47382,
},
TraceEvent::Call {
index: 1,
method: "unknown".to_string(),
arguments: None,
to: contract2_id,
amount: 0,
gas: 34914,
},
TraceEvent::LogData {
index: 2,
id: contract2_id,
value: Some("0x00000000000000000000000000000001".to_string()),
len: 8,
},
TraceEvent::ReturnData {
index: 3,
id: contract2_id,
data: "()".to_string(),
},
TraceEvent::ReturnData {
index: 4,
id: contract1_id,
data: "()".to_string(),
},
TraceEvent::Return {
index: 5,
id: ContractId::zeroed(),
val: 1,
},
TraceEvent::ScriptResult {
index: 6,
result: ScriptExecutionResult::Success,
gas_used: 38059,
},
];
let mut output = Vec::new();
display_transaction_trace(0, &trace_events, &HashMap::new(), &mut output).unwrap();
let trace_output = String::from_utf8(output).unwrap();
let expected_output = r#"
Traces:
[Script]
├─ [47382] 0x7c05fa2efa56c4bba646af0c48db02cba34e54149785cc692ae7e297f031b12e::unknown()
│ ├─ [34914] 0x7ecdf7b507b33131cac7295af2156fd98cd299c3512ec8c2733f920d5f8e4506::unknown()
│ │ ├─ emit 0x00000000000000000000000000000001
│ │ └─ ← ()
│ └─ ← ()
└─ ← [Return] val: 1
[ScriptResult] result: Success, gas_used: 38059
Transaction successfully executed.
Gas used: 0
"#;
assert_eq!(
normalize(&trace_output),
normalize(expected_output),
"\nExpected:\n{expected_output}\n\nActual:\n{trace_output}\n"
);
}
#[test]
fn test_display_transaction_trace_nested_call_log_success_with_multiple_calls() {
let contract1_id = ContractId::from_str(
"41a231bd983812dd51e5778751fc679461b8f580357515848b3ac9a297c6e8bc",
)
.unwrap();
let contract2_id = ContractId::from_str(
"38bf64bfa5ee78b652a36c70eb89fd97caff5ffb419d0abf1199247c168b730c",
)
.unwrap();
let trace_events = vec![
TraceEvent::Call {
index: 0,
method: "unknown".to_string(),
arguments: None,
to: contract1_id,
amount: 0,
gas: 105141,
},
TraceEvent::Call {
index: 1,
method: "unknown".to_string(),
arguments: None,
to: contract2_id,
amount: 0,
gas: 92530,
},
TraceEvent::ReturnData {
index: 2,
id: contract2_id,
data: "()".to_string(),
},
TraceEvent::LogData {
index: 3,
id: contract1_id,
value: Some("0x00000000000000000000000000000001".to_string()),
len: 25,
},
TraceEvent::Call {
index: 4,
method: "unknown".to_string(),
arguments: None,
to: contract2_id,
amount: 0,
gas: 67314,
},
TraceEvent::ReturnData {
index: 5,
id: contract2_id,
data: "0x00000000000000000000000000000002".to_string(),
},
TraceEvent::LogData {
index: 6,
id: contract1_id,
value: Some("0x00000000000000000000000000000002".to_string()),
len: 8,
},
TraceEvent::LogData {
index: 7,
id: contract1_id,
value: Some("0x00000000000000000000000000000003".to_string()),
len: 12,
},
TraceEvent::Call {
index: 8,
method: "unknown".to_string(),
arguments: None,
to: contract2_id,
amount: 0,
gas: 53729,
},
TraceEvent::ReturnData {
index: 9,
id: contract2_id,
data: "()".to_string(),
},
TraceEvent::ReturnData {
index: 10,
id: contract1_id,
data: "()".to_string(),
},
TraceEvent::Return {
index: 11,
id: ContractId::zeroed(),
val: 1,
},
TraceEvent::ScriptResult {
index: 12,
result: ScriptExecutionResult::Success,
gas_used: 76612,
},
];
let mut output = Vec::new();
display_transaction_trace(0, &trace_events, &HashMap::new(), &mut output).unwrap();
let trace_output = String::from_utf8(output).unwrap();
let expected_output = r#"
Traces:
[Script]
├─ [105141] 0x41a231bd983812dd51e5778751fc679461b8f580357515848b3ac9a297c6e8bc::unknown()
│ ├─ [92530] 0x38bf64bfa5ee78b652a36c70eb89fd97caff5ffb419d0abf1199247c168b730c::unknown()
│ │ └─ ← ()
│ ├─ emit 0x00000000000000000000000000000001
│ ├─ [67314] 0x38bf64bfa5ee78b652a36c70eb89fd97caff5ffb419d0abf1199247c168b730c::unknown()
│ │ └─ ← 0x00000000000000000000000000000002
│ ├─ emit 0x00000000000000000000000000000002
│ ├─ emit 0x00000000000000000000000000000003
│ ├─ [53729] 0x38bf64bfa5ee78b652a36c70eb89fd97caff5ffb419d0abf1199247c168b730c::unknown()
│ │ └─ ← ()
│ └─ ← ()
└─ ← [Return] val: 1
[ScriptResult] result: Success, gas_used: 76612
Transaction successfully executed.
Gas used: 0
"#;
assert_eq!(
normalize(&trace_output),
normalize(expected_output),
"\nExpected:\n{expected_output}\n\nActual:\n{trace_output}\n"
);
}
#[test]
fn test_display_transaction_trace_nested_call_log_revert() {
let contract1_id = ContractId::from_str(
"9a7195648cc46c832e490e9bc15ed929fa82801cc0316d1c8e0965bb5e0260a3",
)
.unwrap();
let contract2_id = ContractId::from_str(
"b56b9921112e2fed854ac85357a4914dab561eed98fed0cbe35c1871971dc129",
)
.unwrap();
let trace_events = vec![
TraceEvent::Call {
index: 0,
method: "unknown".to_string(),
arguments: None,
to: contract1_id,
amount: 0,
gas: 46590,
},
TraceEvent::Call {
index: 1,
method: "unknown".to_string(),
arguments: None,
to: contract2_id,
amount: 0,
gas: 34124,
},
TraceEvent::LogData {
index: 2,
id: contract2_id,
value: Some("0x00000000000000000000000000000001".to_string()),
len: 8,
},
TraceEvent::Revert {
index: 3,
id: contract2_id,
ra: 0,
revert_info: None,
},
TraceEvent::ScriptResult {
index: 4,
result: ScriptExecutionResult::Revert,
gas_used: 37531,
},
];
let mut output = Vec::new();
display_transaction_trace(0, &trace_events, &HashMap::new(), &mut output).unwrap();
let trace_output = String::from_utf8(output).unwrap();
let expected_output = r#"
Traces:
[Script]
├─ [46590] 0x9a7195648cc46c832e490e9bc15ed929fa82801cc0316d1c8e0965bb5e0260a3::unknown()
│ ├─ [34124] 0xb56b9921112e2fed854ac85357a4914dab561eed98fed0cbe35c1871971dc129::unknown()
│ │ ├─ emit 0x00000000000000000000000000000001
│ │ └─ ← [Revert]
[ScriptResult] result: Revert, gas_used: 37531
Transaction failed.
Gas used: 0
"#;
assert_eq!(
normalize(&trace_output),
normalize(expected_output),
"\nExpected:\n{expected_output}\n\nActual:\n{trace_output}\n"
);
}
#[test]
fn test_display_transaction_trace_nested_call_log_revert_with_info() {
let contract1_id = ContractId::from_str(
"9a7195648cc46c832e490e9bc15ed929fa82801cc0316d1c8e0965bb5e0260a3",
)
.unwrap();
let contract2_id = ContractId::from_str(
"b56b9921112e2fed854ac85357a4914dab561eed98fed0cbe35c1871971dc129",
)
.unwrap();
let trace_events = vec![
TraceEvent::Call {
index: 0,
method: "unknown".to_string(),
arguments: None,
to: contract1_id,
amount: 0,
gas: 46590,
},
TraceEvent::Call {
index: 1,
method: "unknown".to_string(),
arguments: None,
to: contract2_id,
amount: 0,
gas: 34124,
},
TraceEvent::LogData {
index: 2,
id: contract2_id,
value: Some("0x00000000000000000000000000000001".to_string()),
len: 8,
},
TraceEvent::Revert {
index: 3,
id: contract2_id,
ra: 0,
revert_info: Some(RevertInfoSummary {
revert_code: 0xdeadbeef,
message: Some("boom".to_string()),
value: None,
location: Some(super::PanicLocation {
function: "panic_fn".to_string(),
pkg: "pkg".to_string(),
file: "file.sw".to_string(),
line: 10,
column: 5,
}),
backtrace: vec![super::PanickingCall {
pos: fuel_abi_types::abi::program::ErrorPosition {
pkg: "pkg".to_string(),
file: "file.sw".to_string(),
line: 20,
column: 7,
function: "caller_fn".to_string(),
},
function: "caller_fn".to_string(),
}],
is_known_error: false,
is_raw: false,
}),
},
TraceEvent::ScriptResult {
index: 4,
result: ScriptExecutionResult::Revert,
gas_used: 37531,
},
];
let mut output = Vec::new();
display_transaction_trace(0, &trace_events, &HashMap::new(), &mut output).unwrap();
let trace_output = String::from_utf8(output).unwrap();
let expected_output = r#"
Traces:
[Script]
├─ [46590] 0x9a7195648cc46c832e490e9bc15ed929fa82801cc0316d1c8e0965bb5e0260a3::unknown()
│ ├─ [34124] 0xb56b9921112e2fed854ac85357a4914dab561eed98fed0cbe35c1871971dc129::unknown()
│ │ ├─ emit 0x00000000000000000000000000000001
│ │ └─ ← [Revert]
│ │ ├─ revert code: deadbeef
│ │ ├─ panic message: boom
│ │ ├─ panicked: in panic_fn
│ │ │ └─ at pkg, file.sw:10:5
│ │ └─ backtrace: called in caller_fn
│ │ └─ at pkg, file.sw:20:7
[ScriptResult] result: Revert, gas_used: 37531
Transaction failed.
Gas used: 0
"#;
assert_eq!(
normalize(&trace_output),
normalize(expected_output),
"\nExpected:\n{expected_output}\n\nActual:\n{trace_output}\n"
);
}
#[test]
fn test_display_transaction_trace_nested_call_log_panic() {
let contract1_id = ContractId::from_str(
"b09d73495f6c211ff3586a0542d5fe5fbd45a80e1cd2c1a9a787d6865cc65984",
)
.unwrap();
let contract2_id = ContractId::from_str(
"75c5015d5243cfd798a7f46eb8cf3338e05197e0a271b43c4703764c82d60080",
)
.unwrap();
let trace_events = vec![
TraceEvent::Call {
index: 0,
method: "unknown".to_string(),
arguments: None,
to: contract1_id,
amount: 0,
gas: 25156,
},
TraceEvent::Call {
index: 1,
method: "unknown".to_string(),
arguments: None,
to: contract2_id,
amount: 0,
gas: 12432,
},
TraceEvent::Panic {
index: 2,
id: contract2_id,
reason: "PanicInstruction { reason: MemoryOwnership, instruction: MCP { dst_addr: 0x13, src_addr: 0x14, len: 0x15 } (bytes: 28 4d 45 40) }".to_string(),
contract_id: None,
},
TraceEvent::ScriptResult {
index: 3,
result: ScriptExecutionResult::Panic,
gas_used: 23242,
},
];
let mut output = Vec::new();
display_transaction_trace(0, &trace_events, &HashMap::new(), &mut output).unwrap();
let trace_output = String::from_utf8(output).unwrap();
let expected_output = r#"
Traces:
[Script]
├─ [25156] 0xb09d73495f6c211ff3586a0542d5fe5fbd45a80e1cd2c1a9a787d6865cc65984::unknown()
│ ├─ [12432] 0x75c5015d5243cfd798a7f46eb8cf3338e05197e0a271b43c4703764c82d60080::unknown()
│ │ └─ ← [Panic] PanicInstruction { reason: MemoryOwnership, instruction: MCP { dst_addr: 0x13, src_addr: 0x14, len: 0x15 } (bytes: 28 4d 45 40) }
[ScriptResult] result: Panic, gas_used: 23242
Transaction failed.
Gas used: 0
"#;
assert_eq!(
normalize(&trace_output),
normalize(expected_output),
"\nExpected:\n{expected_output}\n\nActual:\n{trace_output}\n"
);
}
#[test]
fn test_display_transaction_trace_with_labels() {
let contract_id = ContractId::from_str(
"2af09151f8276611ba65f14650970657bc42c1503d6502ffbb4d085ec37065dd",
)
.unwrap();
let trace_events = vec![
TraceEvent::Call {
index: 0,
method: "transfer".to_string(),
arguments: Some(vec!["100".to_string(), "0x123".to_string()]),
to: contract_id,
amount: 0,
gas: 8793,
},
TraceEvent::ReturnData {
index: 1,
id: contract_id,
data: "()".to_string(),
},
TraceEvent::Return {
index: 2,
id: ContractId::zeroed(),
val: 1,
},
TraceEvent::ScriptResult {
index: 3,
result: ScriptExecutionResult::Success,
gas_used: 12400,
},
];
let mut labels = HashMap::new();
labels.insert(contract_id, "TokenContract".to_string());
let mut output = Vec::new();
display_transaction_trace(0, &trace_events, &labels, &mut output).unwrap();
let trace_output = String::from_utf8(output).unwrap();
let expected_output = r#"
Traces:
[Script]
├─ [8793] TokenContract::transfer(100, 0x123)
│ └─ ← ()
└─ ← [Return] val: 1
[ScriptResult] result: Success, gas_used: 12400
Transaction successfully executed.
Gas used: 0
"#;
assert_eq!(
normalize(&trace_output),
normalize(expected_output),
"\nExpected:\n{expected_output}\n\nActual:\n{trace_output}\n"
);
}
}