solify_parser/
lib.rs

1use anyhow::{Context, Result};
2use solify_common::{
3    IdlData, IdlInstruction, IdlAccountItem, IdlField, IdlAccount, IdlTypeDef, 
4    IdlPda, IdlSeed, IdlError, IdlConstant, IdlEvent, ParsedIdl
5};
6
7use solana_sdk::pubkey::Pubkey;
8use std::fs;
9use std::path::Path;
10
11
12pub fn parse_idl<P: AsRef<Path>>(idl_path: P) -> Result<IdlData> {
13    let path = idl_path.as_ref();
14    let idl_content = fs::read_to_string(path)
15        .with_context(|| format!("Failed to read IDL file at {:?}", path))?;
16    let parsed_idl: ParsedIdl = serde_json::from_str(&idl_content)
17        .with_context(|| {
18            if let Err(e) = serde_json::from_str::<serde_json::Value>(&idl_content) {
19                format!("Invalid JSON: {}", e)
20            } else {
21                "Failed to deserialize IDL JSON - structure mismatch".to_string()
22            }
23        })?;
24    
25    convert_to_idl_data(parsed_idl)
26}
27
28fn convert_to_idl_data(parsed: ParsedIdl) -> Result<IdlData> {
29    if parsed.instructions.is_empty() {
30        anyhow::bail!("IDL must have at least one instruction");
31    }
32    
33    Ok(IdlData {
34        name: parsed.metadata.name,
35        version: parsed.metadata.version,
36        instructions: parsed.instructions.into_iter().map(convert_instruction).collect(),
37        accounts: parsed.accounts.into_iter().map(convert_account).collect(),
38        types: parsed.types.into_iter().map(convert_type).collect(),
39        errors: parsed.errors.into_iter().map(convert_error).collect(),
40        constants: parsed.constants.into_iter().map(convert_constant).collect(),
41        events: parsed.events.into_iter().map(convert_event).collect(),
42    })
43}
44
45fn convert_error(error: solify_common::ErrorDef) -> IdlError {
46    IdlError {
47        code: error.code,
48        name: error.name,
49        msg: error.msg,
50    }
51}
52
53fn convert_constant(constant: solify_common::ConstantDef) -> IdlConstant {
54    IdlConstant {
55        name: constant.name,
56        constant_type: constant.constant_type,
57        value: constant.value,
58    }
59}
60
61fn convert_event(event: solify_common::EventDef) -> IdlEvent {
62    IdlEvent {
63        name: event.name,
64        discriminator: event.discriminator,
65        fields: event.fields.into_iter().map(convert_field_def).collect(),
66    }
67}
68
69fn convert_field_def(field: solify_common::FieldDef) -> IdlField {
70    IdlField {
71        name: field.name,
72        field_type: type_to_string(&field.field_type),
73    }
74}
75
76fn convert_instruction(instr: solify_common::Instruction) -> IdlInstruction {
77    IdlInstruction {
78        name: instr.name,
79        accounts: instr.accounts.into_iter().map(convert_account_info).collect(),
80        args: instr.args.into_iter().map(convert_argument).collect(),
81        docs: instr.docs,
82    }
83}
84
85fn convert_account_info(acc: solify_common::AccountInfo) -> IdlAccountItem {
86    IdlAccountItem {
87        name: acc.name,
88        is_mut: acc.writable,
89        is_signer: acc.signer,
90        is_optional: acc.optional,
91        docs: acc.docs,
92        pda: acc.pda.map(convert_pda_config),
93    }
94}
95
96fn convert_pda_config(pda: solify_common::PdaConfig) -> IdlPda {
97    let program = pda.program
98        .and_then(|prog| {
99            if prog.kind == "const" {
100                prog.value.and_then(|bytes| bytes_to_pubkey(&bytes))
101            } else {
102                None
103            }
104        })
105        .unwrap_or_default();
106    
107    IdlPda {
108        seeds: pda.seeds.into_iter().map(convert_pda_seed).collect(),
109        program,
110    }
111}
112
113fn convert_pda_seed(seed: solify_common::PdaSeed) -> IdlSeed {
114    let value = if seed.kind == "const" {
115        seed.value
116            .as_ref()
117            .map(|bytes| {
118                if bytes.len() == 32 {
119                    if let Some(pubkey_str) = bytes_to_pubkey(bytes) {
120                        return pubkey_str;
121                    }
122                }
123                
124                if is_likely_utf8_string(bytes) {
125                    String::from_utf8(bytes.clone())
126                        .unwrap_or_else(|_| bytes_to_hex(bytes))
127                } else {
128                    bytes_to_hex(bytes)
129                }
130            })
131            .unwrap_or_default()
132    } else {
133        String::new()
134    };
135    
136    IdlSeed {
137        kind: seed.kind,
138        path: seed.path,
139        value,
140    }
141}
142
143fn is_likely_utf8_string(bytes: &[u8]) -> bool {
144    if bytes.is_empty() || bytes.len() > 64 {
145        return false;
146    }
147    
148    if let Ok(s) = std::str::from_utf8(bytes) {
149        let printable_count = s.chars().filter(|c| {
150            c.is_alphanumeric() || c.is_whitespace() || "_-./".contains(*c)
151        }).count();
152        
153        let ratio = printable_count as f32 / s.chars().count() as f32;
154        ratio > 0.8
155    } else {
156        false
157    }
158}
159
160fn bytes_to_pubkey(bytes: &[u8]) -> Option<String> {
161    if bytes.len() == 32 {
162        let mut arr = [0u8; 32];
163        arr.copy_from_slice(bytes);
164        let pubkey = Pubkey::new_from_array(arr);
165        Some(pubkey.to_string())
166    } else {
167        None
168    }
169}
170
171fn bytes_to_hex(bytes: &[u8]) -> String {
172    if bytes.len() <= 8 {
173        format!("0x{}", bytes.iter().map(|b| format!("{:02x}", b)).collect::<String>())
174    } else {
175        let preview: String = bytes.iter().take(4).map(|b| format!("{:02x}", b)).collect();
176        format!("0x{}... ({} bytes)", preview, bytes.len())
177    }
178}
179
180fn convert_argument(arg: solify_common::ArgumentDef) -> IdlField {
181    IdlField {
182        name: arg.name,
183        field_type: type_to_string(&arg.arg_type),
184    }
185}
186
187fn convert_account(acc: solify_common::AccountDef) -> IdlAccount {
188    IdlAccount {
189        name: acc.name,
190        fields: vec![],
191    }
192}
193
194fn convert_type(type_def: solify_common::TypeDef) -> IdlTypeDef {
195    match type_def.type_kind {
196        solify_common::TypeKind::Struct { fields } => {
197            IdlTypeDef {
198                name: type_def.name,
199                kind: "struct".to_string(),
200                fields: fields.into_iter().map(|f| f.name).collect(),
201            }
202        }
203        solify_common::TypeKind::Enum { variants } => {
204            IdlTypeDef {
205                name: type_def.name,
206                kind: "enum".to_string(),
207                fields: variants.into_iter().map(|v| v.name).collect(),
208            }
209        }
210    }
211}
212
213fn type_to_string(idl_type: &solify_common::IdlType) -> String {
214    match idl_type {
215        solify_common::IdlType::Simple(s) => s.clone(),
216        solify_common::IdlType::Vec { vec } => {
217            format!("Vec<{}>", type_to_string(vec))
218        }
219        solify_common::IdlType::Option { option } => {
220            format!("Option<{}>", type_to_string(option))
221        }
222        solify_common::IdlType::Array { array } => {
223            let (inner, size) = array;
224            format!("[{}; {}]", type_to_string(inner), size)
225        }
226        solify_common::IdlType::Defined { defined } => {
227            match defined {
228                solify_common::DefinedType::Simple(name) => name.clone(),
229                solify_common::DefinedType::Generic { name, generics } => {
230                    if generics.is_empty() {
231                        name.clone()
232                    } else {
233                        let generic_strs: Vec<String> = generics.iter().map(type_to_string).collect();
234                        format!("{}<{}>", name, generic_strs.join(", "))
235                    }
236                }
237            }
238        }
239    }
240}
241
242
243pub fn get_instruction_names<P: AsRef<Path>>(idl_path: P) -> Result<Vec<String>> {
244    let idl_data = parse_idl(idl_path)?;
245    Ok(idl_data.instructions.iter().map(|i| i.name.clone()).collect())
246}
247
248pub fn find_instruction<'a>(idl_data: &'a IdlData, name: &str) -> Option<&'a IdlInstruction> {
249    idl_data.instructions.iter().find(|i| i.name == name)
250}
251
252pub fn get_pda_accounts(instruction: &IdlInstruction) -> Vec<String> {
253    instruction
254        .accounts
255        .iter()
256        .filter(|acc| acc.pda.is_some())
257        .map(|acc| acc.name.clone())
258        .collect()
259}
260
261pub fn get_signer_accounts(instruction: &IdlInstruction) -> Vec<String> {
262    instruction
263        .accounts
264        .iter()
265        .filter(|acc| acc.is_signer)
266        .map(|acc| acc.name.clone())
267        .collect()
268}
269
270pub fn get_writable_accounts(instruction: &IdlInstruction) -> Vec<String> {
271    instruction
272        .accounts
273        .iter()
274        .filter(|acc| acc.is_mut)
275        .map(|acc| acc.name.clone())
276        .collect()
277}
278
279pub fn get_program_id<P: AsRef<Path>>(idl_path: P) -> Result<String> {
280    let path = idl_path.as_ref();
281    let idl_content = fs::read_to_string(path)
282        .with_context(|| format!("Failed to read IDL file at {:?}", path))?;
283    let parsed_idl: ParsedIdl = serde_json::from_str(&idl_content)
284        .with_context(|| {
285            if let Err(e) = serde_json::from_str::<serde_json::Value>(&idl_content) {
286                format!("Invalid JSON: {}", e)
287            } else {
288                "Failed to deserialize IDL JSON - structure mismatch".to_string()
289            }
290        })?;
291    let program_id = parsed_idl.address;
292    Ok(program_id)
293}