1use serde::{Deserialize, Serialize};
8
9use crate::parser::solana_logs::LogEvent;
10use crate::program_registry::ProgramRegistry;
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
14#[serde(rename_all = "lowercase")]
15pub enum NodeStatus {
16 Success,
18 Failed,
20 Unknown,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct CallNode {
27 pub program_id: String,
29 #[serde(skip_serializing_if = "Option::is_none")]
31 pub label: Option<String>,
32 pub depth: u32,
34 #[serde(skip_serializing_if = "Option::is_none")]
36 pub units_consumed: Option<u64>,
37 pub status: NodeStatus,
39 #[serde(default, skip_serializing_if = "Vec::is_empty")]
41 pub logs: Vec<String>,
42 #[serde(default, skip_serializing_if = "Vec::is_empty")]
44 pub children: Vec<CallNode>,
45}
46
47impl CallNode {
48 fn new(program_id: String, depth: u32, registry: &ProgramRegistry) -> Self {
49 let label = registry.label(&program_id).map(str::to_string);
50 Self {
51 program_id,
52 label,
53 depth,
54 units_consumed: None,
55 status: NodeStatus::Unknown,
56 logs: Vec::new(),
57 children: Vec::new(),
58 }
59 }
60
61 fn root() -> Self {
63 Self {
64 program_id: "transaction".to_string(),
65 label: Some("root transaction".to_string()),
66 depth: 0,
67 units_consumed: None,
68 status: NodeStatus::Unknown,
69 logs: Vec::new(),
70 children: Vec::new(),
71 }
72 }
73
74 #[must_use]
76 pub fn invocation_count(&self) -> u32 {
77 self.children.iter().map(|c| 1 + c.invocation_count()).sum()
78 }
79
80 #[must_use]
82 pub fn max_depth(&self) -> u32 {
83 self.children
84 .iter()
85 .map(CallNode::max_depth)
86 .max()
87 .map_or(self.depth, |d| d.max(self.depth))
88 }
89
90 #[must_use]
92 pub fn cpi_count(&self) -> u32 {
93 let here = u32::from(self.depth >= 2);
94 here + self.children.iter().map(CallNode::cpi_count).sum::<u32>()
95 }
96}
97
98#[must_use]
100pub fn build(events: &[LogEvent], registry: &ProgramRegistry) -> CallNode {
101 let mut stack: Vec<CallNode> = vec![CallNode::root()];
102
103 for event in events {
104 match event {
105 LogEvent::Invoke { program_id, depth } => {
106 stack.push(CallNode::new(program_id.clone(), *depth, registry));
107 }
108 LogEvent::Consumed {
109 program_id, used, ..
110 } => {
111 if let Some(top) = stack.last_mut() {
117 if &top.program_id == program_id {
118 top.units_consumed = Some(*used);
119 }
120 }
121 }
122 LogEvent::Success { .. } => close_top(&mut stack, NodeStatus::Success),
123 LogEvent::Failed { .. } => close_top(&mut stack, NodeStatus::Failed),
124 LogEvent::Log { message } => push_log(&mut stack, message.clone()),
125 LogEvent::ScopeBegin { name, .. } => {
126 push_log(&mut stack, format!("scope-begin: {name}"));
127 }
128 LogEvent::ScopeEnd { name, .. } => push_log(&mut stack, format!("scope-end: {name}")),
129 LogEvent::ScopePoint { name, .. } => {
130 push_log(&mut stack, format!("scope-point: {name}"));
131 }
132 LogEvent::Raw(line) => push_log(&mut stack, line.clone()),
133 }
134 }
135
136 while stack.len() > 1 {
138 close_top(&mut stack, NodeStatus::Unknown);
139 }
140 stack.pop().unwrap_or_else(CallNode::root)
141}
142
143fn close_top(stack: &mut Vec<CallNode>, status: NodeStatus) {
144 if stack.len() <= 1 {
145 return; }
147 let mut node = stack.pop().expect("len > 1 checked");
148 if node.status == NodeStatus::Unknown {
149 node.status = status;
150 }
151 if let Some(parent) = stack.last_mut() {
152 parent.children.push(node);
153 }
154}
155
156fn push_log(stack: &mut [CallNode], message: String) {
157 if let Some(top) = stack.last_mut() {
158 top.logs.push(message);
159 }
160}
161
162#[cfg(test)]
163mod tests {
164 use super::*;
165 use crate::parser::solana_logs::lex;
166
167 fn tree_from(lines: &[&str]) -> CallNode {
168 let owned: Vec<String> = lines.iter().map(|s| (*s).to_string()).collect();
169 let lexed = lex(&owned);
170 let events: Vec<LogEvent> = lexed.events().cloned().collect();
171 build(&events, &ProgramRegistry::with_builtins())
172 }
173
174 #[test]
175 fn builds_nested_cpi_tree() {
176 let tree = tree_from(&[
177 "Program User111 invoke [1]",
178 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA invoke [2]",
179 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA consumed 3000 of 197000 compute units",
180 "Program TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA success",
181 "Program User111 consumed 12000 of 200000 compute units",
182 "Program User111 success",
183 ]);
184 assert_eq!(tree.children.len(), 1);
185 let user = &tree.children[0];
186 assert_eq!(user.program_id, "User111");
187 assert_eq!(user.units_consumed, Some(12000));
188 assert_eq!(user.children.len(), 1);
189 assert_eq!(user.children[0].label.as_deref(), Some("SPL Token"));
190 assert_eq!(tree.cpi_count(), 1);
191 assert_eq!(tree.max_depth(), 2);
192 assert_eq!(tree.invocation_count(), 2);
193 }
194
195 #[test]
196 fn unterminated_invoke_does_not_panic() {
197 let tree = tree_from(&["Program User111 invoke [1]"]);
198 assert_eq!(tree.children.len(), 1);
199 assert_eq!(tree.children[0].status, NodeStatus::Unknown);
200 }
201
202 #[test]
203 fn consumed_attributes_only_to_matching_program() {
204 let tree = tree_from(&[
207 "Program A111 invoke [1]",
208 "Program B222 invoke [2]",
209 "Program A111 consumed 5000 of 200000 compute units", "Program B222 consumed 3000 of 197000 compute units",
211 "Program B222 success",
212 "Program A111 success",
213 ]);
214 let a = &tree.children[0];
215 let b = &a.children[0];
216 assert_eq!(b.program_id, "B222");
217 assert_eq!(b.units_consumed, Some(3000));
219 }
220}