hyperstack_idl/analysis/
type_graph.rs1use crate::types::{IdlSpec, IdlType, IdlTypeDefKind};
4use crate::utils::to_pascal_case;
5
6#[derive(Debug, Clone)]
8pub struct PubkeyFieldRef {
9 pub field_name: String,
11 pub likely_target: Option<String>,
15}
16
17#[derive(Debug, Clone)]
19pub struct TypeNode {
20 pub type_name: String,
22 pub pubkey_fields: Vec<PubkeyFieldRef>,
24}
25
26pub fn extract_type_graph(idl: &IdlSpec) -> Vec<TypeNode> {
35 let account_names: Vec<&str> = idl.accounts.iter().map(|a| a.name.as_str()).collect();
36
37 let mut nodes = Vec::new();
38
39 for type_def in &idl.types {
40 let fields = match &type_def.type_def {
41 IdlTypeDefKind::Struct { fields, .. } => fields,
42 _ => continue,
43 };
44
45 let pubkey_fields: Vec<PubkeyFieldRef> = fields
46 .iter()
47 .filter(|f| is_pubkey_type(&f.type_))
48 .map(|f| {
49 let likely_target = infer_target(&f.name, &account_names);
50 PubkeyFieldRef {
51 field_name: f.name.clone(),
52 likely_target,
53 }
54 })
55 .collect();
56
57 if !pubkey_fields.is_empty() {
58 nodes.push(TypeNode {
59 type_name: type_def.name.clone(),
60 pubkey_fields,
61 });
62 }
63 }
64
65 nodes
66}
67
68fn is_pubkey_type(ty: &IdlType) -> bool {
70 matches!(ty, IdlType::Simple(s) if s == "pubkey" || s == "publicKey")
71}
72
73fn infer_target(field_name: &str, account_names: &[&str]) -> Option<String> {
78 let candidates = stripped_candidates(field_name);
79
80 for candidate in &candidates {
81 let pascal = to_pascal_case(candidate);
82 for &acct in account_names {
83 if acct.eq_ignore_ascii_case(&pascal) {
84 return Some(acct.to_string());
85 }
86 }
87 }
88
89 None
90}
91
92fn stripped_candidates(field_name: &str) -> Vec<&str> {
94 let mut candidates = vec![field_name];
95
96 for suffix in &["_id", "_key"] {
97 if let Some(stripped) = field_name.strip_suffix(suffix) {
98 if !stripped.is_empty() {
99 candidates.push(stripped);
100 }
101 }
102 }
103
104 candidates
105}
106
107#[cfg(test)]
108mod tests {
109 use super::*;
110 use crate::parse::parse_idl_file;
111 use std::path::PathBuf;
112
113 fn meteora_fixture() -> IdlSpec {
114 let path =
115 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/meteora_dlmm.json");
116 parse_idl_file(&path).expect("should parse meteora_dlmm.json")
117 }
118
119 #[test]
120 fn test_type_graph() {
121 let idl = meteora_fixture();
122 let graph = extract_type_graph(&idl);
123
124 assert!(
126 !graph.is_empty(),
127 "should extract type nodes with pubkey fields"
128 );
129
130 let position = graph.iter().find(|n| n.type_name == "Position");
132 assert!(
133 position.is_some(),
134 "Position type should be in the type graph"
135 );
136 let position = position.unwrap();
137
138 let lb_pair_field = position
140 .pubkey_fields
141 .iter()
142 .find(|f| f.field_name == "lb_pair");
143 assert!(
144 lb_pair_field.is_some(),
145 "Position should have lb_pair pubkey field"
146 );
147 assert_eq!(
148 lb_pair_field.unwrap().likely_target.as_deref(),
149 Some("LbPair"),
150 "lb_pair should resolve to LbPair account type"
151 );
152
153 let owner_field = position
155 .pubkey_fields
156 .iter()
157 .find(|f| f.field_name == "owner");
158 assert!(
159 owner_field.is_some(),
160 "Position should have owner pubkey field"
161 );
162
163 println!("Type graph nodes: {}", graph.len());
165 for node in &graph {
166 println!(
167 " {} — pubkey fields: {:?}",
168 node.type_name,
169 node.pubkey_fields
170 .iter()
171 .map(|f| format!(
172 "{} -> {}",
173 f.field_name,
174 f.likely_target.as_deref().unwrap_or("?")
175 ))
176 .collect::<Vec<_>>()
177 );
178 }
179 }
180
181 #[test]
182 fn test_is_pubkey_type() {
183 assert!(is_pubkey_type(&IdlType::Simple("pubkey".to_string())));
184 assert!(is_pubkey_type(&IdlType::Simple("publicKey".to_string())));
185 assert!(!is_pubkey_type(&IdlType::Simple("u64".to_string())));
186 assert!(!is_pubkey_type(&IdlType::Simple("bool".to_string())));
187 }
188
189 #[test]
190 fn test_stripped_candidates() {
191 assert_eq!(stripped_candidates("lb_pair"), vec!["lb_pair"]);
192 assert_eq!(stripped_candidates("pool_id"), vec!["pool_id", "pool"]);
193 assert_eq!(stripped_candidates("mint_key"), vec!["mint_key", "mint"]);
194 assert_eq!(stripped_candidates("_id"), vec!["_id"]);
195 }
196
197 #[test]
198 fn test_infer_target() {
199 let accounts = vec!["LbPair", "Position", "BinArray"];
200
201 assert_eq!(
202 infer_target("lb_pair", &accounts),
203 Some("LbPair".to_string())
204 );
205 assert_eq!(
206 infer_target("position", &accounts),
207 Some("Position".to_string())
208 );
209 assert_eq!(
210 infer_target("bin_array_id", &accounts),
211 Some("BinArray".to_string()),
212 "should match after stripping _id suffix"
213 );
214 assert_eq!(
215 infer_target("unknown_field", &accounts),
216 None,
217 "should return None for non-matching fields"
218 );
219 }
220}