hyperstack_idl/analysis/
relations.rs1use 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}