Skip to main content

hyperstack_idl/analysis/
pda_graph.rs

1//! PDA graph analysis — extracts PDA derivation info from IDL instructions.
2
3use crate::types::{IdlPdaSeed, IdlSpec};
4
5/// Classification of a PDA seed.
6#[derive(Debug, Clone, PartialEq, Eq)]
7pub enum SeedKind {
8    /// Constant byte array seed (often a UTF-8 string like "pool", "lb_pair").
9    Const,
10    /// Reference to another account in the instruction.
11    Account,
12    /// Reference to an instruction argument.
13    Arg,
14}
15
16/// A single seed in a PDA derivation.
17#[derive(Debug, Clone)]
18pub struct PdaSeedInfo {
19    pub kind: SeedKind,
20    /// For `Const`: UTF-8 decoded string or hex representation.
21    /// For `Account`/`Arg`: the path field (e.g. "lb_pair", "base_mint").
22    pub value: String,
23}
24
25/// A PDA node linking an account, its instruction context, and derivation seeds.
26#[derive(Debug, Clone)]
27pub struct PdaNode {
28    pub account_name: String,
29    pub instruction_name: String,
30    pub seeds: Vec<PdaSeedInfo>,
31}
32
33/// Extract all PDA derivation nodes from an IDL spec.
34///
35/// Iterates every instruction's account list, collecting accounts that have
36/// a `pda` field with seeds. Each seed is classified and its value extracted.
37pub 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
60/// Extract kind and human-readable value from an `IdlPdaSeed`.
61fn extract_seed_info(seed: &IdlPdaSeed) -> PdaSeedInfo {
62    match seed {
63        IdlPdaSeed::Const { value } => {
64            // Try to decode byte array as UTF-8; fall back to hex representation
65            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
82/// Simple hex encoding for non-UTF-8 byte arrays.
83fn 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        // meteora_dlmm has many PDA accounts
101        assert!(
102            !graph.is_empty(),
103            "should extract PDA nodes from meteora_dlmm"
104        );
105
106        // Check that at least some nodes have seeds
107        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        // Verify seed kinds are present
111        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        // Const seeds should decode to readable strings (e.g. "oracle", "preset_parameter")
129        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        // Print summary for evidence
144        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        // Print a few sample nodes
176        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}