use atupa_core::TraceStep;
use atupa_output::SvgGenerator;
use atupa_parser::{Parser as AtupaParser, aggregator::Aggregator};
use atupa_rpc::EthClient;
use colored::*;
use indicatif::{ProgressBar, ProgressStyle};
use std::fs;
use std::time::Duration;
use atupa_rpc::etherscan::EtherscanResolver;
pub async fn execute_profile(
tx: &str,
rpc: &str,
is_demo: bool,
out: Option<String>,
etherscan_key: Option<String>,
) -> anyhow::Result<()> {
let spinner = initialize_spinner()?;
let steps = if is_demo {
get_demo_trace(&spinner)
} else {
fetch_live_trace(tx, rpc, &spinner).await
};
let aggregate_step_msg = if is_demo { "[2/2]" } else { "[3/4]" };
spinner.set_message(format!(
"Aggregating execution metrics... {}",
aggregate_step_msg
));
let mut stacks = Aggregator::build_collapsed_stacks(&steps);
spinner.set_message("Resolving Contract Alignments against Etherscan...");
let resolver = EtherscanResolver::new(etherscan_key);
for stack in &mut stacks {
if let Some(addr) = &stack.target_address {
if let Some(name) = resolver.resolve_contract_name(addr).await {
stack.target_address = Some(name);
}
}
}
render_and_save_trace(stacks, tx, is_demo, out, &spinner)?;
Ok(())
}
fn initialize_spinner() -> anyhow::Result<ProgressBar> {
let spinner = ProgressBar::new_spinner();
spinner.set_style(
ProgressStyle::default_spinner()
.tick_chars("⠁⠂⠄⡀⢀⠠⠐⠈ ")
.template("{spinner:.cyan} {msg}")?,
);
spinner.enable_steady_tick(Duration::from_millis(100));
Ok(spinner)
}
fn get_demo_trace(spinner: &ProgressBar) -> Vec<TraceStep> {
spinner.set_message("Generating offline demo trace... [1/2]");
vec![
TraceStep {
pc: 0,
op: "PUSH1".into(),
gas: 1000,
gas_cost: 3,
depth: 1,
stack: None,
memory: None,
error: None,
reverted: false,
},
TraceStep {
pc: 1,
op: "CALL".into(),
gas: 997,
gas_cost: 2600,
depth: 1,
stack: Some(vec![
"0x0000000000000000000000000000000000000000".into(), "0x0000000000000000000000000000000000000000".into(), "0x0000000000000000000000000000000000000000".into(), "0x0000000000000000000000000000000000000100".into(), "0x0000000000000000000000000000000000000000".into(), "0x000000000000000000000000a0b86991c6218b36c1d19d4a2e9eb0ce3606eb48".into(), "0x10000".into(), ]),
memory: None,
error: None,
reverted: false,
},
TraceStep {
pc: 0,
op: "SLOAD".into(),
gas: 500,
gas_cost: 2100,
depth: 2,
stack: None,
memory: None,
error: None,
reverted: false,
},
TraceStep {
pc: 1,
op: "SSTORE".into(),
gas: 480,
gas_cost: 20000,
depth: 2,
stack: None,
memory: None,
error: None,
reverted: false,
},
TraceStep {
pc: 2,
op: "REVERT".into(),
gas: 400,
gas_cost: 5000,
depth: 2,
stack: None,
memory: None,
error: None,
reverted: true,
}, TraceStep {
pc: 2,
op: "STOP".into(),
gas: 300,
gas_cost: 0,
depth: 1,
stack: None,
memory: None,
error: None,
reverted: false,
},
]
}
async fn fetch_live_trace(tx: &str, rpc: &str, spinner: &ProgressBar) -> Vec<TraceStep> {
spinner.set_message("Connecting to EVM Node via JSON-RPC... [1/4]");
let client = EthClient::new(rpc.to_string());
let trace_res =
match tokio::time::timeout(Duration::from_secs(15), client.get_transaction_trace(tx)).await
{
Ok(Ok(res)) => res,
Ok(Err(e)) => {
spinner.finish_and_clear();
eprintln!(
"\n{} Could not fetch trace from node.",
"Error:".bold().red()
);
eprintln!(
"{} Verify your RPC node is running at {}?",
"Hint:".cyan(),
rpc.yellow().bold()
);
eprintln!("{} {}", "Details:".dimmed(), e);
std::process::exit(1);
}
Err(_) => {
spinner.finish_and_clear();
eprintln!(
"\n{} Connection to RPC node timed out after 15 seconds.",
"Timeout:".bold().red()
);
eprintln!(
"{} Is the node fully synced and responding at {}?",
"Hint:".cyan(),
rpc.yellow().bold()
);
std::process::exit(1);
}
};
spinner.set_message(format!(
"Normalizing {} structLogs... [2/4]",
trace_res.struct_logs.len()
));
AtupaParser::normalize(trace_res.struct_logs)
}
fn render_and_save_trace(
stacks: Vec<atupa_core::CollapsedStack>,
tx: &str,
is_demo: bool,
out: Option<String>,
spinner: &ProgressBar,
) -> anyhow::Result<()> {
let output_step_msg = if is_demo { "[Done]" } else { "[4/4]" };
spinner.set_message(format!(
"Generating visual flamegraph... {}",
output_step_msg
));
let svg = SvgGenerator::generate_flamegraph(&stacks)?;
let out_file =
out.unwrap_or_else(|| format!("profile_{}.svg", if is_demo { "demo" } else { tx }));
fs::write(&out_file, svg)?;
spinner.finish_with_message(format!(
"{} Profile saved to {}",
"Success!".bold().green(),
out_file.bold()
));
Ok(())
}