use std::{
collections::{BTreeMap, HashMap, HashSet},
path::PathBuf,
};
use chrono::{DateTime, Utc};
use clap::Parser;
use console::style;
use eyre::{Context, Result};
use crate::output::SimulationOutput;
#[derive(Parser, Debug)]
pub struct SummarizeArgs {
pub file: PathBuf,
#[arg(long, value_delimiter = ',')]
pub accounts: Vec<String>,
}
struct BalanceTrace {
opening: u64,
closing: u64,
decimals: u8,
}
pub fn summarize(args: SummarizeArgs) -> Result<()> {
let json = std::fs::read_to_string(&args.file)
.with_context(|| format!("failed to read {}", args.file.display()))?;
let output: SimulationOutput = serde_json::from_str(&json).with_context(|| {
format!(
"failed to parse {} as simulation output",
args.file.display()
)
})?;
let m = &output.metadata;
println!("File : {}", args.file.display());
println!("Slot range : {} → {}", m.start_slot, m.end_slot);
if !m.program_ids.is_empty() {
println!("Program IDs : {}", m.program_ids.join(", "));
}
if !m.program_so.is_empty() {
println!("Program.so : {}", m.program_so.join(", "));
}
println!("Session(s) : {}", m.session_ids.len());
let ran_at = DateTime::<Utc>::from_timestamp(m.ran_at_unix_secs as i64, 0)
.map(|dt| dt.format("%Y-%m-%d %H:%M:%S UTC").to_string())
.unwrap_or_else(|| format!("{} (unix)", m.ran_at_unix_secs));
println!("Ran at : {ran_at}");
println!();
let s = &output.summary;
println!(
"{} Transactions : {} ({} successes, {} failures)",
style("i").blue(),
s.total_transactions,
s.successes,
s.failures,
);
if args.accounts.is_empty() {
return Ok(());
}
let tracked: HashSet<&str> = args.accounts.iter().map(|s| s.as_str()).collect();
let mut traces: HashMap<&str, BTreeMap<String, BalanceTrace>> =
tracked.iter().map(|a| (*a, BTreeMap::new())).collect();
for tx in &output.transactions {
for sc in &tx.sol_changes {
if let Some(wallet) = tracked.get(sc.pubkey.as_str()) {
let entry = traces
.get_mut(wallet)
.unwrap()
.entry("SOL".to_string())
.or_insert(BalanceTrace {
opening: sc.pre_lamports,
closing: sc.post_lamports,
decimals: 9,
});
entry.closing = sc.post_lamports;
}
}
for tc in &tx.token_changes {
if let Some(wallet) = tracked.get(tc.owner.as_str()) {
let entry = traces
.get_mut(wallet)
.unwrap()
.entry(tc.mint.clone())
.or_insert(BalanceTrace {
opening: tc.pre_amount,
closing: tc.post_amount,
decimals: tc.decimals,
});
entry.closing = tc.post_amount;
}
}
}
println!();
for account in &args.accounts {
println!("Account: {account}");
let wallet_traces = &traces[account.as_str()];
if wallet_traces.is_empty() {
println!(" (no activity found)");
}
for (label, t) in wallet_traces {
let dec = t.decimals as usize;
let div = 10u64.pow(t.decimals as u32) as f64;
let open = t.opening as f64 / div;
let close = t.closing as f64 / div;
let delta = close - open;
println!(" {label:<50} {open:.dec$} → {close:.dec$} (Δ {delta:+.dec$})");
}
println!();
}
Ok(())
}