hyperstack_idl/analysis/
pda_graph.rs1use crate::types::{IdlPdaSeed, IdlSpec};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum SeedKind {
8 Const,
10 Account,
12 Arg,
14}
15
16#[derive(Debug, Clone)]
18pub struct PdaSeedInfo {
19 pub kind: SeedKind,
20 pub value: String,
23}
24
25#[derive(Debug, Clone)]
27pub struct PdaNode {
28 pub account_name: String,
29 pub instruction_name: String,
30 pub seeds: Vec<PdaSeedInfo>,
31}
32
33pub fn extract_pda_graph(idl: &IdlSpec) -> Vec<PdaNode> {
38 let mut nodes = Vec::new();
39
40 for ix in &idl.instructions {
41 for acc in &ix.accounts {
42 if let Some(pda) = &acc.pda {
43 let seeds = pda.seeds.iter().map(extract_seed_info).collect();
44
45 nodes.push(PdaNode {
46 account_name: acc.name.clone(),
47 instruction_name: ix.name.clone(),
48 seeds,
49 });
50 }
51 }
52 }
53 nodes
54}
55
56fn extract_seed_info(seed: &IdlPdaSeed) -> PdaSeedInfo {
58 match seed {
59 IdlPdaSeed::Const { value } => {
60 let decoded = String::from_utf8(value.clone()).unwrap_or_else(|_| hex_encode(value));
62 PdaSeedInfo {
63 kind: SeedKind::Const,
64 value: decoded,
65 }
66 }
67 IdlPdaSeed::Account { path, .. } => PdaSeedInfo {
68 kind: SeedKind::Account,
69 value: path.clone(),
70 },
71 IdlPdaSeed::Arg { path, .. } => PdaSeedInfo {
72 kind: SeedKind::Arg,
73 value: path.clone(),
74 },
75 }
76}
77
78fn hex_encode(bytes: &[u8]) -> String {
80 bytes.iter().map(|b| format!("{:02x}", b)).collect()
81}
82
83#[cfg(test)]
84mod tests {
85 use super::*;
86 use crate::parse::parse_idl_file;
87 use std::path::PathBuf;
88
89 #[test]
90 fn test_pda_graph() {
91 let path =
92 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/meteora_dlmm.json");
93 let idl = parse_idl_file(&path).expect("should parse");
94 let graph = extract_pda_graph(&idl);
95
96 assert!(
98 !graph.is_empty(),
99 "should extract PDA nodes from meteora_dlmm"
100 );
101
102 let with_seeds = graph.iter().filter(|n| !n.seeds.is_empty()).count();
104 assert!(with_seeds > 0, "some PDA nodes should have seeds");
105
106 let has_account_seed = graph
108 .iter()
109 .flat_map(|n| &n.seeds)
110 .any(|s| s.kind == SeedKind::Account);
111 let has_arg_seed = graph
112 .iter()
113 .flat_map(|n| &n.seeds)
114 .any(|s| s.kind == SeedKind::Arg);
115 let has_const_seed = graph
116 .iter()
117 .flat_map(|n| &n.seeds)
118 .any(|s| s.kind == SeedKind::Const);
119
120 assert!(has_account_seed, "should have Account seeds");
121 assert!(has_arg_seed, "should have Arg seeds");
122 assert!(has_const_seed, "should have Const seeds");
123
124 let const_seeds: Vec<&str> = graph
126 .iter()
127 .flat_map(|n| &n.seeds)
128 .filter(|s| s.kind == SeedKind::Const)
129 .map(|s| s.value.as_str())
130 .collect();
131 assert!(
132 const_seeds
133 .iter()
134 .any(|s| s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')),
135 "at least one const seed should be a readable ASCII string, got: {:?}",
136 const_seeds
137 );
138
139 println!("PDA graph nodes: {}", graph.len());
141 println!("Nodes with seeds: {}", with_seeds);
142 println!(
143 "Account seeds: {}",
144 graph
145 .iter()
146 .flat_map(|n| &n.seeds)
147 .filter(|s| s.kind == SeedKind::Account)
148 .count()
149 );
150 println!(
151 "Arg seeds: {}",
152 graph
153 .iter()
154 .flat_map(|n| &n.seeds)
155 .filter(|s| s.kind == SeedKind::Arg)
156 .count()
157 );
158 println!(
159 "Const seeds: {}",
160 graph
161 .iter()
162 .flat_map(|n| &n.seeds)
163 .filter(|s| s.kind == SeedKind::Const)
164 .count()
165 );
166 println!(
167 "Sample const values: {:?}",
168 &const_seeds[..const_seeds.len().min(10)]
169 );
170
171 for node in graph.iter().take(5) {
173 println!(
174 " {} in {}: {:?}",
175 node.account_name,
176 node.instruction_name,
177 node.seeds
178 .iter()
179 .map(|s| format!("{:?}={}", s.kind, s.value))
180 .collect::<Vec<_>>()
181 );
182 }
183 }
184}