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}