use crate::decoder::{extract_params, fetch_function_name};
use crate::ens::get_ens_name;
use crate::types::{EventDetails, EventResponse};
use crate::utils::{draw_gas_circle, format_dynsol_value};
use alloy::{
dyn_abi::{DynSolType, DynSolValue},
hex,
primitives::utils::format_units,
providers::{ext::DebugApi, Provider, RootProvider},
rpc::types::{
trace::geth::GethDebugTracingOptions, BlockTransactionsKind, Log, Transaction,
TransactionReceipt,
},
transports::http::Http,
};
use chrono::{prelude::DateTime, Utc};
use colored::Colorize;
use comfy_table::{presets::UTF8_FULL, Attribute, Cell, Color, ContentArrangement, Table};
use reqwest::Client;
use std::{
collections::HashMap,
time::{Duration, UNIX_EPOCH},
};
pub async fn print_transaction(
val: Transaction,
rec: TransactionReceipt,
provider: RootProvider<Http<Client>>,
logs_stats: bool,
traces_stats: bool,
) {
let chain_id = provider.get_chain_id().await.unwrap();
let block_time = provider
.get_block_by_hash(val.block_hash.unwrap(), BlockTransactionsKind::Hashes)
.await
.unwrap()
.unwrap()
.header
.timestamp;
let d = UNIX_EPOCH + Duration::from_secs(block_time);
let datetime = DateTime::<Utc>::from(d);
let timestamp_str = format!(
"{}({})",
block_time,
datetime.format("%Y-%m-%dT%H:%M:%SZ").to_string()
);
let details = format_transaction_data(&val, &rec, &provider, chain_id, ×tamp_str).await;
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_width(100)
.set_header(vec![
Cell::new("Field")
.add_attribute(Attribute::Bold)
.fg(Color::Green),
Cell::new("Value")
.add_attribute(Attribute::Bold)
.fg(Color::Green),
]);
for (key, value) in details {
table.add_row(vec![
Cell::new(key).fg(Color::Green),
Cell::new(value).fg(Color::Cyan),
]);
}
println!("{}", table);
if logs_stats {
let logs_data = format_logs_data(rec.inner.logs()).await;
print_logs_from_data(logs_data);
}
if traces_stats {
match provider
.debug_trace_transaction(rec.transaction_hash, GethDebugTracingOptions::default())
.await
{
Ok(resp) => {
let file_name = format!("./trace_{}_{}.json", chain_id, rec.transaction_hash);
match serde_json::to_string_pretty(&resp) {
Ok(json) => match std::fs::write(&file_name, json) {
Ok(_) => println!("Trace saved to {}", file_name.blue()),
Err(e) => eprintln!("Failed to write to file: {}", e),
},
Err(e) => eprintln!("Failed to serialize trace to JSON: {}", e),
}
}
Err(_) => println!("Traces fetch failed - please use premium rpc"),
}
}
}
pub async fn format_transaction_data(
val: &Transaction,
rec: &TransactionReceipt,
provider: &RootProvider<Http<Client>>,
chain_id: u64,
timestamp_str: &str,
) -> Vec<(String, String)> {
let mut data = Vec::new();
data.push(("From".to_string(), val.from.to_string()));
if chain_id == 1 {
if let Some(ens_name) = get_ens_name(val.from, provider).await {
data.push(("Sender ENS name".to_string(), ens_name));
}
}
data.push((
"Block number".to_string(),
val.block_number.unwrap().to_string(),
));
data.push(("Block Time".to_string(), timestamp_str.to_string()));
data.push((
"Block hash".to_string(),
val.block_hash.unwrap().to_string(),
));
data.push((
"Transaction index".to_string(),
val.transaction_index.unwrap().to_string(),
));
data.push((
"Effective gas price".to_string(),
format!(
"{} Gwei",
format_units(val.effective_gas_price.unwrap(), "gwei").unwrap()
),
));
data.push((
"Status".to_string(),
if rec.status() {
"Success".to_string()
} else {
"Failed".to_string()
},
));
if let Some(legacy) = val.inner.as_legacy() {
data.push(("Transaction type".to_string(), format!("{} - (Legacy)", 0)));
data.push(("Nonce".to_string(), legacy.tx().nonce.to_string()));
data.push((
"Gas Price".to_string(),
format!(
"{} Gwei",
format_units(legacy.tx().gas_price, "gwei").unwrap()
),
));
data.push((
"Gas Used / Gas Limit".to_string(),
format!(
"{} / {} ({}% - {})",
rec.gas_used,
legacy.tx().gas_limit,
format!(
"{:.2}",
((rec.gas_used * 100) as f64 / (legacy.tx().gas_limit) as f64)
),
draw_gas_circle(rec.gas_used, legacy.tx().gas_limit as u128)
),
));
match legacy.tx().to.to() {
Some(to_address) => {
data.push(("To Address".to_string(), to_address.to_string()));
if chain_id == 1 {
if let Some(ens_name) = get_ens_name(*to_address, provider).await {
data.push(("Receiver ENS name".to_string(), ens_name));
}
}
}
None => {
data.push((
"Contract Deployed".to_string(),
rec.contract_address.unwrap().to_string(),
));
}
}
data.push((
"Value".to_string(),
format!("{} ETH", format_units(legacy.tx().value, "ether").unwrap()),
));
data.push(("Input".to_string(), legacy.tx().input.to_string()));
if legacy.tx().to.to().is_some() {
match fetch_function_name(&legacy.tx().input.to_string()).await {
Some((name, params)) => {
data.push(("Function Name".to_string(), name));
for (i, val) in params.iter().enumerate() {
data.push((format!("Param {}", i), format_dynsol_value(val)));
}
}
None => {
data.push((
"Function Name".to_string(),
"No function name found".to_string(),
));
}
}
}
} else if let Some(eip2930) = val.inner.as_eip2930() {
data.push((
"Transaction type".to_string(),
format!("{} - (EIP-2930)", 1),
));
data.push(("Nonce".to_string(), eip2930.tx().nonce.to_string()));
data.push((
"Gas Price".to_string(),
format!(
"{} Gwei",
format_units(eip2930.tx().gas_price, "gwei").unwrap()
),
));
data.push((
"Gas Used / Gas Limit".to_string(),
format!(
"{} / {} ({}% - {})",
rec.gas_used,
eip2930.tx().gas_limit,
format!(
"{:.2}",
((rec.gas_used * 100) as f64 / (eip2930.tx().gas_limit) as f64)
),
draw_gas_circle(rec.gas_used, eip2930.tx().gas_limit as u128)
),
));
match eip2930.tx().to.to() {
Some(to_address) => {
data.push(("To Address".to_string(), to_address.to_string()));
if chain_id == 1 {
if let Some(ens_name) = get_ens_name(*to_address, provider).await {
data.push(("Receiver ENS name".to_string(), ens_name));
}
}
}
None => {
data.push((
"Contract Deployed".to_string(),
rec.contract_address.unwrap().to_string(),
));
}
}
data.push((
"Value".to_string(),
format!("{} ETH", format_units(eip2930.tx().value, "ether").unwrap()),
));
data.push(("Input".to_string(), eip2930.tx().input.to_string()));
data.push((
"Accesslist".to_string(),
format!("{:#?}", eip2930.tx().access_list.to_vec()),
));
if eip2930.tx().to.to().is_some() {
match fetch_function_name(&eip2930.tx().input.to_string()).await {
Some((name, params)) => {
data.push(("Function Name".to_string(), name));
for (i, val) in params.iter().enumerate() {
data.push((format!("Param {}", i), format_dynsol_value(val)));
}
}
None => {
data.push((
"Function Name".to_string(),
"No function name found".to_string(),
));
}
}
}
} else if let Some(eip1559) = val.inner.as_eip1559() {
data.push((
"Transaction type".to_string(),
format!("{} - (EIP-1559)", 2),
));
data.push(("Nonce".to_string(), eip1559.tx().nonce.to_string()));
data.push((
"Max Fee Per Gas".to_string(),
format!(
"{} Gwei",
format_units(eip1559.tx().max_fee_per_gas, "gwei").unwrap()
),
));
data.push((
"Max Priority Fee Per Gas".to_string(),
format!(
"{} Gwei",
format_units(eip1559.tx().max_priority_fee_per_gas, "gwei").unwrap()
),
));
data.push((
"Gas Used / Gas Limit".to_string(),
format!(
"{} / {} ({}% - {})",
rec.gas_used,
eip1559.tx().gas_limit,
format!(
"{:.2}",
((rec.gas_used * 100) as f64 / (eip1559.tx().gas_limit) as f64)
),
draw_gas_circle(rec.gas_used, eip1559.tx().gas_limit as u128)
),
));
match eip1559.tx().to.to() {
Some(to_address) => {
data.push(("To Address".to_string(), to_address.to_string()));
if chain_id == 1 {
if let Some(ens_name) = get_ens_name(*to_address, provider).await {
data.push(("Receiver ENS name".to_string(), ens_name));
}
}
}
None => {
data.push((
"Contract Deployed".to_string(),
rec.contract_address.unwrap().to_string(),
));
}
}
data.push((
"Value".to_string(),
format!("{} ETH", format_units(eip1559.tx().value, "ether").unwrap()),
));
data.push(("Input".to_string(), eip1559.tx().input.to_string()));
if eip1559.tx().to.to().is_some() {
match fetch_function_name(&eip1559.tx().input.to_string()).await {
Some((name, params)) => {
data.push(("Function Name".to_string(), name));
for (i, val) in params.iter().enumerate() {
data.push((format!("Param {}", i), format_dynsol_value(val)));
}
}
None => {
data.push((
"Function Name".to_string(),
"No function name found".to_string(),
));
}
}
}
} else if let Some(blob) = val.inner.as_eip4844() {
data.push((
"Transaction type".to_string(),
format!("{} - (EIP-4844)", 3),
));
data.push(("Nonce".to_string(), blob.tx().tx().nonce.to_string()));
data.push((
"Max Fee Per Gas".to_string(),
format!(
"{} Gwei",
format_units(blob.tx().tx().max_fee_per_gas, "gwei").unwrap()
),
));
data.push((
"Max Priority Fee Per Gas".to_string(),
format!(
"{} Gwei",
format_units(blob.tx().tx().max_priority_fee_per_gas, "gwei").unwrap()
),
));
data.push((
"Gas Used / Gas Limit".to_string(),
format!(
"{} / {} ({}% - {})",
rec.gas_used,
blob.tx().tx().gas_limit,
format!(
"{:.2}",
((rec.gas_used * 100) as f64 / (blob.tx().tx().gas_limit) as f64)
),
draw_gas_circle(rec.gas_used, blob.tx().tx().gas_limit as u128)
),
));
data.push(("To Address".to_string(), blob.tx().tx().to.to_string()));
if chain_id == 1 {
if let Some(ens_name) = get_ens_name(blob.tx().tx().to, provider).await {
data.push(("Receiver ENS name".to_string(), ens_name));
}
}
data.push((
"Value".to_string(),
format!(
"{} ETH",
format_units(blob.tx().tx().value, "ether").unwrap()
),
));
data.push(("Input".to_string(), blob.tx().tx().input.to_string()));
match fetch_function_name(&blob.tx().tx().input.to_string()).await {
Some((name, params)) => {
data.push(("Function Name".to_string(), name));
for (i, val) in params.iter().enumerate() {
data.push((format!("Param {}", i), format_dynsol_value(val)));
}
}
None => {
data.push((
"Function Name".to_string(),
"No function name found".to_string(),
));
}
}
} else if let Some(eip7702) = val.inner.as_eip7702() {
data.push((
"Transaction type".to_string(),
format!("{} - (EIP-7702)", 4),
));
data.push(("Nonce".to_string(), eip7702.tx().nonce.to_string()));
data.push((
"Max Fee Per Gas".to_string(),
format!(
"{} Gwei",
format_units(eip7702.tx().max_fee_per_gas, "gwei").unwrap()
),
));
data.push((
"Max Priority Fee Per Gas".to_string(),
format!(
"{} Gwei",
format_units(eip7702.tx().max_priority_fee_per_gas, "gwei").unwrap()
),
));
data.push((
"Gas Used / Gas Limit".to_string(),
format!(
"{} / {} ({}% - {})",
rec.gas_used,
eip7702.tx().gas_limit,
format!(
"{:.2}",
((rec.gas_used * 100) as f64 / (eip7702.tx().gas_limit) as f64)
),
draw_gas_circle(rec.gas_used, eip7702.tx().gas_limit as u128)
),
));
data.push(("To Address".to_string(), eip7702.tx().to.to_string()));
if chain_id == 1 {
if let Some(ens_name) = get_ens_name(eip7702.tx().to, provider).await {
data.push(("Receiver ENS name".to_string(), ens_name));
}
}
data.push((
"Value".to_string(),
format!("{} ETH", format_units(eip7702.tx().value, "ether").unwrap()),
));
data.push(("Input".to_string(), eip7702.tx().input.to_string()));
if !eip7702.tx().authorization_list.is_empty() {
data.push(("Authorizations".to_string(), "".to_string()));
for (i, item) in eip7702.tx().authorization_list.iter().enumerate() {
data.push((format!("Addr {}", i), item.address.to_string()));
}
}
{
match fetch_function_name(&eip7702.tx().input.to_string()).await {
Some((name, params)) => {
data.push(("Function Name".to_string(), name));
for (i, val) in params.iter().enumerate() {
data.push((format!("Param {}", i), format_dynsol_value(val)));
}
}
None => {
data.push((
"Function Name".to_string(),
"No function name found".to_string(),
));
}
}
}
} else {
data.push((
"Error".to_string(),
format!("Unknown transaction type: {:?}", val.inner),
));
}
data
}
pub async fn format_logs_data(logs: &[Log]) -> Vec<(String, Vec<(String, String)>)> {
let mut formatted_logs = Vec::new();
let unique_topics: Vec<_> = logs
.iter()
.filter_map(|log| log.topics().get(0).cloned())
.collect();
let mut topic_map: HashMap<String, Vec<EventDetails>> = HashMap::new();
if !unique_topics.is_empty() {
let query = unique_topics
.iter()
.map(|t| format!("0x{:x}", t))
.collect::<Vec<_>>()
.join(",");
let url = format!(
"https://api.openchain.xyz/signature-database/v1/lookup?event={}&filter=true",
query
);
if let Ok(resp) = reqwest::get(&url).await {
if let Ok(text) = resp.text().await {
if let Ok(parsed) = serde_json::from_str::<EventResponse>(&text) {
topic_map = parsed.result.event;
}
}
}
}
for (i, log) in logs.iter().enumerate() {
let mut log_details = Vec::new();
let log_title = format!("Log #{}", i);
let topics = log.topics();
if topics.is_empty() {
log_details.push(("Status".to_string(), "No topics found".to_string()));
formatted_logs.push((log_title, log_details));
continue;
}
let topic0_str = format!("0x{:x}", topics[0]);
if let Some(events) = topic_map.get(&topic0_str) {
if let Some(event_sig) = events.get(0).map(|e| e.name.clone()) {
log_details.push(("Event Signature".to_string(), event_sig.clone()));
if let Some(param_types) = extract_params(&event_sig) {
let mut final_data = String::new();
for topic in &topics[1..] {
final_data.push_str(&format!("{:x}", topic));
}
final_data.push_str(&log.data().data.to_string()[2..]);
let (indexed_types, non_indexed_types): (Vec<_>, Vec<_>) = param_types
.into_iter()
.enumerate()
.partition(|(i, _)| *i < topics.len() - 1);
let indexed_values: Vec<_> = indexed_types
.into_iter()
.zip(topics.iter().skip(1))
.map(|((_, ty), topic)| {
ty.abi_decode(topic.as_ref())
.unwrap_or(DynSolValue::String("Failed to decode topic".into()))
})
.collect();
let non_indexed_values = if !log.data().data.is_empty() {
let param_type = DynSolType::Tuple(
non_indexed_types.into_iter().map(|(_, t)| t).collect(),
);
let decoded = param_type
.abi_decode_params(
&hex::decode(&log.data().data.to_string()[2..]).unwrap(),
)
.unwrap_or(DynSolValue::Tuple(vec![]));
if let DynSolValue::Tuple(vals) = decoded {
vals
} else {
vec![]
}
} else {
vec![]
};
for (j, val) in indexed_values
.iter()
.chain(non_indexed_values.iter())
.enumerate()
{
log_details.push((format!("Param {}", j), format_dynsol_value(val)));
}
} else {
log_details.push((
"Error".to_string(),
"Unable to parse parameter types".to_string(),
));
log_details.push(("Raw Data".to_string(), format!("{:#?}", log.data())));
}
} else {
log_details.push((
"Warning".to_string(),
"Event signature not recognized".to_string(),
));
log_details.push(("Raw Data".to_string(), format!("{:#?}", log.data())));
}
} else {
log_details.push((
"Warning".to_string(),
"No event data found for topic".to_string(),
));
log_details.push(("Raw Data".to_string(), format!("{:#?}", log.data())));
}
formatted_logs.push((log_title, log_details));
}
formatted_logs
}
fn print_logs_from_data(logs_data: Vec<(String, Vec<(String, String)>)>) {
println!("\n{}", "Logs".green().bold());
for (title, details) in logs_data {
let mut table = Table::new();
table
.load_preset(UTF8_FULL)
.set_content_arrangement(ContentArrangement::Dynamic)
.set_width(100)
.set_header(vec![
Cell::new(title)
.add_attribute(Attribute::Bold)
.fg(Color::Green),
Cell::new("Details")
.add_attribute(Attribute::Bold)
.fg(Color::Green),
]);
for (key, value) in details {
table.add_row(vec![
Cell::new(key).fg(Color::Green),
Cell::new(value).fg(Color::Cyan),
]);
}
println!("{}", table);
}
}