use serde::Serialize;
use crate::analyze::{AccountDiff, Explanation};
use crate::cpi_tree::{IxAccount, IxArg};
use crate::replay::ReplayResult;
#[derive(Debug, Clone, Serialize)]
pub struct Trace {
pub signature: String,
pub fee_payer: String,
pub steps: Vec<Step>,
pub result: ReplayResult,
#[serde(skip_serializing_if = "Option::is_none")]
pub explain: Option<Explanation>,
#[serde(skip_serializing_if = "Option::is_none")]
pub failed_step: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub clock: Option<String>,
pub fidelity: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub onchain_success: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tier: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tier_note: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub state_slot: Option<u64>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub drifted: Vec<DriftedAccount>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
pub struct DriftedAccount {
pub address: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_write_slot: Option<u64>,
pub changed_since_slot: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct Step {
pub path: String,
pub depth: u8,
pub index: usize,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub original_index: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub diffs_since: Option<usize>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub data_hex: Option<String>,
pub program: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub args: Vec<IxArg>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub accounts: Vec<IxAccount>,
#[serde(skip_serializing_if = "Option::is_none")]
pub cu_consumed: Option<u64>,
pub logs: (usize, usize),
#[serde(skip_serializing_if = "Vec::is_empty")]
pub diffs: Vec<AccountDiff>,
pub state_known: bool,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<StepError>,
#[serde(skip_serializing_if = "Option::is_none")]
pub return_data: Option<ReturnData>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub events: Vec<DecodedEvent>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub state: Vec<StepAccountState>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub prefix_artifact: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct ReturnData {
pub program: String,
pub data_base64: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DecodedEvent {
pub name: String,
pub fields: Vec<crate::decode::Field>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct StepAccountState {
pub address: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub role: Option<String>,
pub owner: String,
pub lamports: u64,
pub data_len: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub type_name: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub fields: Vec<crate::decode::Field>,
pub changed: bool,
pub exists: bool,
}
#[derive(Debug, Clone, Serialize)]
pub struct StepError {
pub raw: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub explain: Option<Explanation>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct StepSummary {
pub program: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub cu_consumed: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
pub changes: Vec<(String, String, String, String)>,
}
#[derive(Debug, Clone, Serialize)]
pub struct TraceDiff {
pub outcome_changed: bool,
pub steps: Vec<StepDiff>,
}
#[derive(Debug, Clone, Serialize)]
pub struct StepDiff {
pub path: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub before: Option<StepSummary>,
#[serde(skip_serializing_if = "Option::is_none")]
pub after: Option<StepSummary>,
}
impl Step {
fn summary(&self) -> StepSummary {
let mut changes: Vec<(String, String, String, String)> = self
.diffs
.iter()
.flat_map(|d| {
let addr = d.address.clone();
let mut v: Vec<_> = d
.fields
.iter()
.map(|f| {
(
addr.clone(),
f.name.clone(),
f.before.clone(),
f.after.clone(),
)
})
.collect();
if d.lamports_before != d.lamports_after {
v.push((
addr.clone(),
"lamports".into(),
d.lamports_before.to_string(),
d.lamports_after.to_string(),
));
}
if d.raw_data_changed && d.fields.is_empty() {
v.push((addr, "data".into(), "…".into(), "changed".into()));
}
v
})
.collect();
changes.sort();
StepSummary {
program: self.program.clone(),
name: self.name.clone(),
success: self.success,
cu_consumed: self.cu_consumed,
error: self.error.as_ref().map(|e| e.raw.clone()),
changes,
}
}
}
impl Trace {
pub fn drifted(&self) -> bool {
matches!(self.onchain_success, Some(o) if o != self.result.success)
}
pub fn failed(&self) -> Option<&Step> {
self.failed_step.and_then(|i| self.steps.get(i))
}
pub fn top_level(&self) -> impl Iterator<Item = &Step> {
self.steps.iter().filter(|s| s.depth == 1)
}
pub fn diff(&self, other: &Trace) -> TraceDiff {
let mut paths: Vec<&str> = self
.steps
.iter()
.chain(other.steps.iter())
.map(|s| s.path.as_str())
.collect();
paths.sort_by_key(|p| path_key(p));
paths.dedup();
let find = |t: &Trace, p: &str| t.steps.iter().find(|s| s.path == p).map(Step::summary);
let steps = paths
.into_iter()
.filter_map(|p| {
let (before, after) = (find(self, p), find(other, p));
(before != after).then(|| StepDiff {
path: p.to_string(),
before,
after,
})
})
.collect();
TraceDiff {
outcome_changed: self.result.success != other.result.success,
steps,
}
}
}
fn path_key(p: &str) -> Vec<usize> {
p.split('.').filter_map(|s| s.parse().ok()).collect()
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct LogSpan {
pub program: String,
pub depth: u8,
pub start: usize,
pub end: usize,
pub cu_consumed: Option<u64>,
pub success: bool,
pub failure: Option<String>,
}
pub(crate) fn spans_from_logs(logs: &[String], from: usize) -> Vec<LogSpan> {
let mut spans: Vec<LogSpan> = Vec::new();
let mut stack: Vec<usize> = Vec::new(); for (i, line) in logs.iter().enumerate().skip(from) {
let Some(rest) = line.strip_prefix("Program ") else {
continue;
};
if rest.starts_with("log: ") || rest.starts_with("data: ") || rest.starts_with("return: ") {
continue;
}
if let Some(pos) = rest.find(" invoke [") {
let program = rest[..pos].to_string();
let depth = rest[pos + 9..]
.trim_end_matches(']')
.parse::<u8>()
.unwrap_or(stack.len() as u8 + 1);
spans.push(LogSpan {
program,
depth,
start: i,
end: i + 1,
..Default::default()
});
stack.push(spans.len() - 1);
} else if let Some(pos) = rest.find(" consumed ") {
let program = &rest[..pos];
let cu = rest[pos + 10..]
.split(' ')
.next()
.and_then(|n| n.parse::<u64>().ok());
if let Some(&top) = stack.iter().rev().find(|&&s| spans[s].program == program) {
spans[top].cu_consumed = cu;
}
} else if let Some(program) = rest.strip_suffix(" success") {
if let Some(idx) = pop_matching(&mut stack, &spans, program) {
spans[idx].success = true;
spans[idx].end = i + 1;
}
} else if let Some(pos) = rest.find(" failed: ") {
let program = &rest[..pos];
if let Some(idx) = pop_matching(&mut stack, &spans, program) {
spans[idx].success = false;
spans[idx].failure = Some(rest[pos + 9..].to_string());
spans[idx].end = i + 1;
}
}
}
for idx in stack {
spans[idx].end = logs.len();
}
spans
}
fn pop_matching(stack: &mut Vec<usize>, spans: &[LogSpan], program: &str) -> Option<usize> {
let pos = stack.iter().rposition(|&s| spans[s].program == program)?;
let idx = stack[pos];
stack.truncate(pos);
Some(idx)
}
#[cfg(test)]
mod tests {
use super::*;
fn lines(v: &[&str]) -> Vec<String> {
v.iter().map(|s| s.to_string()).collect()
}
#[test]
fn spans_reconstruct_nested_tree() {
let logs = lines(&[
"Program A invoke [1]",
"Program log: hi",
"Program B invoke [2]",
"Program B consumed 100 of 200 compute units",
"Program B success",
"Program A consumed 500 of 1000 compute units",
"Program A success",
"Program C invoke [1]",
"Program C failed: custom program error: 0x1771",
]);
let s = spans_from_logs(&logs, 0);
assert_eq!(s.len(), 3);
assert_eq!(
(s[0].program.as_str(), s[0].depth, s[0].start, s[0].end),
("A", 1, 0, 7)
);
assert_eq!(s[0].cu_consumed, Some(500));
assert!(s[0].success);
assert_eq!(
(s[1].program.as_str(), s[1].depth, s[1].start, s[1].end),
("B", 2, 2, 5)
);
assert_eq!(s[1].cu_consumed, Some(100));
assert_eq!((s[2].program.as_str(), s[2].depth), ("C", 1));
assert!(!s[2].success);
assert_eq!(
s[2].failure.as_deref(),
Some("custom program error: 0x1771")
);
}
#[test]
fn spans_start_from_offset_and_survive_truncation() {
let logs = lines(&[
"Program A invoke [1]",
"Program A success",
"Program B invoke [1]",
"Program log: …",
]);
let s = spans_from_logs(&logs, 2);
assert_eq!(s.len(), 1);
assert_eq!((s[0].program.as_str(), s[0].start, s[0].end), ("B", 2, 4));
assert!(!s[0].success);
}
#[test]
fn path_ordering_is_numeric() {
let mut v = vec!["10", "2", "2.1", "2.0", "0"];
v.sort_by_key(|p| path_key(p));
assert_eq!(v, vec!["0", "2", "2.0", "2.1", "10"]);
}
}