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.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
56/// Extract kind and human-readable value from an `IdlPdaSeed`.
57fn extract_seed_info(seed: &IdlPdaSeed) -> PdaSeedInfo {
58    match seed {
59        IdlPdaSeed::Const { value } => {
60            // Try to decode byte array as UTF-8; fall back to hex representation
61            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
78/// Simple hex encoding for non-UTF-8 byte arrays.
79fn 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        // meteora_dlmm has many PDA accounts
97        assert!(
98            !graph.is_empty(),
99            "should extract PDA nodes from meteora_dlmm"
100        );
101
102        // Check that at least some nodes have seeds
103        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        // Verify seed kinds are present
107        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        // Const seeds should decode to readable strings (e.g. "oracle", "preset_parameter")
125        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        // Print summary for evidence
140        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        // Print a few sample nodes
172        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}