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
44 .seeds
45 .iter()
46 .map(extract_seed_info)
47 .collect();
48
49 nodes.push(PdaNode {
50 account_name: acc.name.clone(),
51 instruction_name: ix.name.clone(),
52 seeds,
53 });
54 }
55 }
56 }
57 nodes
58}
59
60fn extract_seed_info(seed: &IdlPdaSeed) -> PdaSeedInfo {
62 match seed {
63 IdlPdaSeed::Const { value } => {
64 let decoded = String::from_utf8(value.clone()).unwrap_or_else(|_| hex_encode(value));
66 PdaSeedInfo {
67 kind: SeedKind::Const,
68 value: decoded,
69 }
70 }
71 IdlPdaSeed::Account { path, .. } => PdaSeedInfo {
72 kind: SeedKind::Account,
73 value: path.clone(),
74 },
75 IdlPdaSeed::Arg { path, .. } => PdaSeedInfo {
76 kind: SeedKind::Arg,
77 value: path.clone(),
78 },
79 }
80}
81
82fn hex_encode(bytes: &[u8]) -> String {
84 bytes.iter().map(|b| format!("{:02x}", b)).collect()
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90 use crate::parse::parse_idl_file;
91 use std::path::PathBuf;
92
93 #[test]
94 fn test_pda_graph() {
95 let path =
96 PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/meteora_dlmm.json");
97 let idl = parse_idl_file(&path).expect("should parse");
98 let graph = extract_pda_graph(&idl);
99
100 assert!(
102 !graph.is_empty(),
103 "should extract PDA nodes from meteora_dlmm"
104 );
105
106 let with_seeds = graph.iter().filter(|n| !n.seeds.is_empty()).count();
108 assert!(with_seeds > 0, "some PDA nodes should have seeds");
109
110 let has_account_seed = graph
112 .iter()
113 .flat_map(|n| &n.seeds)
114 .any(|s| s.kind == SeedKind::Account);
115 let has_arg_seed = graph
116 .iter()
117 .flat_map(|n| &n.seeds)
118 .any(|s| s.kind == SeedKind::Arg);
119 let has_const_seed = graph
120 .iter()
121 .flat_map(|n| &n.seeds)
122 .any(|s| s.kind == SeedKind::Const);
123
124 assert!(has_account_seed, "should have Account seeds");
125 assert!(has_arg_seed, "should have Arg seeds");
126 assert!(has_const_seed, "should have Const seeds");
127
128 let const_seeds: Vec<&str> = graph
130 .iter()
131 .flat_map(|n| &n.seeds)
132 .filter(|s| s.kind == SeedKind::Const)
133 .map(|s| s.value.as_str())
134 .collect();
135 assert!(
136 const_seeds
137 .iter()
138 .any(|s| s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')),
139 "at least one const seed should be a readable ASCII string, got: {:?}",
140 const_seeds
141 );
142
143 println!("PDA graph nodes: {}", graph.len());
145 println!("Nodes with seeds: {}", with_seeds);
146 println!(
147 "Account seeds: {}",
148 graph
149 .iter()
150 .flat_map(|n| &n.seeds)
151 .filter(|s| s.kind == SeedKind::Account)
152 .count()
153 );
154 println!(
155 "Arg seeds: {}",
156 graph
157 .iter()
158 .flat_map(|n| &n.seeds)
159 .filter(|s| s.kind == SeedKind::Arg)
160 .count()
161 );
162 println!(
163 "Const seeds: {}",
164 graph
165 .iter()
166 .flat_map(|n| &n.seeds)
167 .filter(|s| s.kind == SeedKind::Const)
168 .count()
169 );
170 println!(
171 "Sample const values: {:?}",
172 &const_seeds[..const_seeds.len().min(10)]
173 );
174
175 for node in graph.iter().take(5) {
177 println!(
178 " {} in {}: {:?}",
179 node.account_name,
180 node.instruction_name,
181 node.seeds
182 .iter()
183 .map(|s| format!("{:?}={}", s.kind, s.value))
184 .collect::<Vec<_>>()
185 );
186 }
187 }
188}