Skip to main content

hyperstack_idl/analysis/
relations.rs

1use crate::types::IdlSpec;
2use crate::utils::to_pascal_case;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone)]
6pub struct InstructionRef {
7    pub name: String,
8    pub account_count: usize,
9    pub arg_count: usize,
10}
11
12#[derive(Debug, Clone)]
13pub struct AccountUsage {
14    pub account_name: String,
15    pub instructions: Vec<InstructionRef>,
16    pub is_writable: bool,
17    pub is_signer: bool,
18    pub is_pda: bool,
19}
20
21#[derive(Debug, Clone, PartialEq)]
22pub enum AccountCategory {
23    Entity,
24    Infrastructure,
25    Role,
26    Other,
27}
28
29#[derive(Debug, Clone)]
30pub struct AccountRelation {
31    pub account_name: String,
32    pub matched_type: Option<String>,
33    pub instruction_count: usize,
34    pub category: AccountCategory,
35}
36
37#[derive(Debug, Clone)]
38pub struct InstructionLink {
39    pub instruction_name: String,
40    pub account_a_writable: bool,
41    pub account_b_writable: bool,
42}
43
44const INFRASTRUCTURE_ACCOUNTS: &[&str] = &[
45    "system_program",
46    "token_program",
47    "rent",
48    "event_authority",
49    "program",
50    "associated_token_program",
51    "memo_program",
52    "token_2022_program",
53    "clock",
54    "instructions",
55    "sysvar_instructions",
56];
57
58const ROLE_ACCOUNTS: &[&str] = &[
59    "authority",
60    "owner",
61    "payer",
62    "signer",
63    "admin",
64    "user",
65    "sender",
66    "receiver",
67];
68
69pub fn build_account_index(idl: &IdlSpec) -> HashMap<String, AccountUsage> {
70    let mut index: HashMap<String, AccountUsage> = HashMap::new();
71
72    for ix in &idl.instructions {
73        let ix_ref = InstructionRef {
74            name: ix.name.clone(),
75            account_count: ix.accounts.len(),
76            arg_count: ix.args.len(),
77        };
78        for acc in &ix.accounts {
79            let entry = index
80                .entry(acc.name.clone())
81                .or_insert_with(|| AccountUsage {
82                    account_name: acc.name.clone(),
83                    instructions: Vec::new(),
84                    is_writable: false,
85                    is_signer: false,
86                    is_pda: false,
87                });
88            entry.instructions.push(ix_ref.clone());
89            if acc.is_mut {
90                entry.is_writable = true;
91            }
92            if acc.is_signer {
93                entry.is_signer = true;
94            }
95            if acc.pda.is_some() {
96                entry.is_pda = true;
97            }
98        }
99    }
100    index
101}
102
103pub fn classify_accounts(idl: &IdlSpec) -> Vec<AccountRelation> {
104    let index = build_account_index(idl);
105    let type_names: Vec<String> = idl.accounts.iter().map(|a| a.name.clone()).collect();
106
107    index
108        .into_values()
109        .map(|usage| {
110            let pascal = to_pascal_case(&usage.account_name);
111            let matched_type = type_names
112                .iter()
113                .find(|t| **t == pascal || **t == usage.account_name)
114                .cloned();
115
116            let category = if INFRASTRUCTURE_ACCOUNTS.contains(&usage.account_name.as_str()) {
117                AccountCategory::Infrastructure
118            } else if matched_type.is_some() {
119                AccountCategory::Entity
120            } else if ROLE_ACCOUNTS.iter().any(|r| usage.account_name.contains(r))
121                || usage.is_signer
122            {
123                AccountCategory::Role
124            } else {
125                AccountCategory::Other
126            };
127
128            AccountRelation {
129                account_name: usage.account_name.clone(),
130                matched_type,
131                instruction_count: usage.instructions.len(),
132                category,
133            }
134        })
135        .collect()
136}
137
138pub fn find_account_usage(idl: &IdlSpec, account_name: &str) -> Option<AccountUsage> {
139    let index = build_account_index(idl);
140    index.into_values().find(|u| u.account_name == account_name)
141}
142
143pub fn find_links(idl: &IdlSpec, account_a: &str, account_b: &str) -> Vec<InstructionLink> {
144    idl.instructions
145        .iter()
146        .filter(|ix| {
147            let names: Vec<&str> = ix.accounts.iter().map(|a| a.name.as_str()).collect();
148            names.contains(&account_a) && names.contains(&account_b)
149        })
150        .map(|ix| {
151            let a_writable = ix
152                .accounts
153                .iter()
154                .find(|a| a.name == account_a)
155                .map(|a| a.is_mut)
156                .unwrap_or(false);
157            let b_writable = ix
158                .accounts
159                .iter()
160                .find(|a| a.name == account_b)
161                .map(|a| a.is_mut)
162                .unwrap_or(false);
163            InstructionLink {
164                instruction_name: ix.name.clone(),
165                account_a_writable: a_writable,
166                account_b_writable: b_writable,
167            }
168        })
169        .collect()
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::parse::parse_idl_file;
176    use std::path::PathBuf;
177
178    fn meteora_fixture() -> IdlSpec {
179        let path =
180            PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/meteora_dlmm.json");
181        parse_idl_file(&path).expect("should parse meteora_dlmm.json")
182    }
183
184    #[test]
185    fn test_classify_accounts_dlmm() {
186        let idl = meteora_fixture();
187        let relations = classify_accounts(&idl);
188        let lb_pair = relations.iter().find(|r| r.account_name == "lb_pair");
189        assert!(lb_pair.is_some(), "lb_pair should be in relations");
190        assert_eq!(
191            lb_pair.expect("lb_pair relation should exist").category,
192            AccountCategory::Entity,
193            "lb_pair should be Entity"
194        );
195
196        let sys = relations
197            .iter()
198            .find(|r| r.account_name == "system_program");
199        if let Some(sys) = sys {
200            assert_eq!(sys.category, AccountCategory::Infrastructure);
201        }
202    }
203
204    #[test]
205    fn test_find_links() {
206        let idl = meteora_fixture();
207        let links = find_links(&idl, "lb_pair", "position");
208        assert!(
209            !links.is_empty(),
210            "lb_pair and position should share instructions"
211        );
212    }
213
214    #[test]
215    fn test_build_account_index() {
216        let idl = meteora_fixture();
217        let index = build_account_index(&idl);
218        let lb_pair = index.get("lb_pair");
219        assert!(lb_pair.is_some(), "lb_pair should be in index");
220        assert!(
221            lb_pair
222                .expect("lb_pair account usage should exist")
223                .instructions
224                .len()
225                > 10,
226            "lb_pair should appear in many instructions"
227        );
228    }
229}