solscript_codegen/
idl_gen.rs

1//! IDL Generator
2//!
3//! Generates Anchor IDL (Interface Definition Language) JSON for the program.
4
5use crate::ir::*;
6use crate::CodegenError;
7use serde::Serialize;
8
9/// IDL generator
10pub struct IdlGenerator {
11    program_name: String,
12}
13
14impl Default for IdlGenerator {
15    fn default() -> Self {
16        Self::new()
17    }
18}
19
20impl IdlGenerator {
21    pub fn new() -> Self {
22        Self {
23            program_name: String::new(),
24        }
25    }
26
27    /// Generate the IDL JSON
28    pub fn generate(&mut self, ir: &SolanaProgram) -> Result<String, CodegenError> {
29        self.program_name = to_snake_case(&ir.name);
30
31        let idl = Idl {
32            version: "0.1.0".to_string(),
33            name: self.program_name.clone(),
34            instructions: self.generate_instructions(ir)?,
35            accounts: self.generate_accounts(ir)?,
36            types: self.generate_types(ir)?,
37            events: self.generate_events(ir)?,
38            errors: self.generate_errors(ir)?,
39            metadata: IdlMetadata {
40                address: "11111111111111111111111111111111".to_string(),
41            },
42        };
43
44        serde_json::to_string_pretty(&idl)
45            .map_err(|e| CodegenError::GenerationFailed(format!("Failed to serialize IDL: {}", e)))
46    }
47
48    fn generate_instructions(
49        &self,
50        ir: &SolanaProgram,
51    ) -> Result<Vec<IdlInstruction>, CodegenError> {
52        let mut instructions = Vec::new();
53
54        for instr in &ir.instructions {
55            let args: Vec<IdlField> = instr
56                .params
57                .iter()
58                .map(|p| IdlField {
59                    name: to_camel_case_lower(&p.name),
60                    ty: self.solana_type_to_idl_type(&p.ty),
61                })
62                .collect();
63
64            // Build accounts list
65            let mut accounts = vec![
66                IdlAccount {
67                    name: "state".to_string(),
68                    is_mut: !instr.is_view,
69                    is_signer: false,
70                },
71                IdlAccount {
72                    name: "signer".to_string(),
73                    is_mut: true,
74                    is_signer: true,
75                },
76            ];
77
78            // Add system program for initialize
79            if instr.name.to_lowercase() == "initialize" {
80                accounts.push(IdlAccount {
81                    name: "systemProgram".to_string(),
82                    is_mut: false,
83                    is_signer: false,
84                });
85            }
86
87            // Add mapping accounts
88            for (i, access) in instr.mapping_accesses.iter().enumerate() {
89                accounts.push(IdlAccount {
90                    name: format!("{}_entry_{}", to_camel_case_lower(&access.mapping_name), i),
91                    is_mut: !instr.is_view,
92                    is_signer: false,
93                });
94            }
95
96            instructions.push(IdlInstruction {
97                name: to_camel_case_lower(&instr.name),
98                accounts,
99                args,
100                returns: instr
101                    .returns
102                    .as_ref()
103                    .map(|t| self.solana_type_to_idl_type(t)),
104            });
105        }
106
107        Ok(instructions)
108    }
109
110    fn generate_accounts(&self, ir: &SolanaProgram) -> Result<Vec<IdlAccountDef>, CodegenError> {
111        let mut accounts = Vec::new();
112
113        // Main state account
114        let state_fields: Vec<IdlField> = ir
115            .state
116            .fields
117            .iter()
118            .map(|f| IdlField {
119                name: to_camel_case_lower(&f.name),
120                ty: self.solana_type_to_idl_type(&f.ty),
121            })
122            .collect();
123
124        accounts.push(IdlAccountDef {
125            name: format!("{}State", to_camel_case(&self.program_name)),
126            ty: IdlAccountType {
127                kind: "struct".to_string(),
128                fields: state_fields,
129            },
130        });
131
132        // Mapping entry accounts
133        for mapping in &ir.mappings {
134            accounts.push(IdlAccountDef {
135                name: format!("{}Entry", to_camel_case(&mapping.name)),
136                ty: IdlAccountType {
137                    kind: "struct".to_string(),
138                    fields: vec![IdlField {
139                        name: "value".to_string(),
140                        ty: self.solana_type_to_idl_type(&mapping.value_ty),
141                    }],
142                },
143            });
144        }
145
146        Ok(accounts)
147    }
148
149    fn generate_types(&self, ir: &SolanaProgram) -> Result<Vec<IdlTypeDef>, CodegenError> {
150        let mut types = Vec::new();
151
152        // Structs
153        for s in &ir.structs {
154            let fields: Vec<IdlField> = s
155                .fields
156                .iter()
157                .map(|f| IdlField {
158                    name: to_camel_case_lower(&f.name),
159                    ty: self.solana_type_to_idl_type(&f.ty),
160                })
161                .collect();
162
163            types.push(IdlTypeDef {
164                name: s.name.clone(),
165                ty: IdlTypeDefType::Struct { fields },
166            });
167        }
168
169        // Enums
170        for e in &ir.enums {
171            let variants: Vec<IdlEnumVariant> = e
172                .variants
173                .iter()
174                .map(|v| IdlEnumVariant { name: v.clone() })
175                .collect();
176
177            types.push(IdlTypeDef {
178                name: e.name.clone(),
179                ty: IdlTypeDefType::Enum { variants },
180            });
181        }
182
183        Ok(types)
184    }
185
186    fn generate_events(&self, ir: &SolanaProgram) -> Result<Vec<IdlEvent>, CodegenError> {
187        let mut events = Vec::new();
188
189        for event in &ir.events {
190            let fields: Vec<IdlEventField> = event
191                .fields
192                .iter()
193                .map(|f| IdlEventField {
194                    name: to_camel_case_lower(&f.name),
195                    ty: self.solana_type_to_idl_type(&f.ty),
196                    index: f.indexed,
197                })
198                .collect();
199
200            events.push(IdlEvent {
201                name: event.name.clone(),
202                fields,
203            });
204        }
205
206        Ok(events)
207    }
208
209    fn generate_errors(&self, ir: &SolanaProgram) -> Result<Vec<IdlError>, CodegenError> {
210        let mut errors = Vec::new();
211
212        // Built-in error
213        errors.push(IdlError {
214            code: 6000,
215            name: "RequireFailed".to_string(),
216            msg: "Requirement failed".to_string(),
217        });
218
219        // Custom errors
220        for (i, error) in ir.errors.iter().enumerate() {
221            errors.push(IdlError {
222                code: 6001 + i as u32,
223                name: error.name.clone(),
224                msg: error.name.clone(),
225            });
226        }
227
228        Ok(errors)
229    }
230
231    fn solana_type_to_idl_type(&self, ty: &SolanaType) -> IdlType {
232        match ty {
233            SolanaType::U8 => IdlType::Primitive("u8".to_string()),
234            SolanaType::U16 => IdlType::Primitive("u16".to_string()),
235            SolanaType::U32 => IdlType::Primitive("u32".to_string()),
236            SolanaType::U64 => IdlType::Primitive("u64".to_string()),
237            SolanaType::U128 => IdlType::Primitive("u128".to_string()),
238            SolanaType::I8 => IdlType::Primitive("i8".to_string()),
239            SolanaType::I16 => IdlType::Primitive("i16".to_string()),
240            SolanaType::I32 => IdlType::Primitive("i32".to_string()),
241            SolanaType::I64 => IdlType::Primitive("i64".to_string()),
242            SolanaType::I128 => IdlType::Primitive("i128".to_string()),
243            SolanaType::Bool => IdlType::Primitive("bool".to_string()),
244            SolanaType::String => IdlType::Primitive("string".to_string()),
245            SolanaType::Pubkey => IdlType::Primitive("publicKey".to_string()),
246            SolanaType::Signer => IdlType::Primitive("publicKey".to_string()),
247            SolanaType::Bytes => IdlType::Primitive("bytes".to_string()),
248            SolanaType::FixedBytes(n) => IdlType::Array {
249                array: (Box::new(IdlType::Primitive("u8".to_string())), *n),
250            },
251            SolanaType::Array(inner, size) => IdlType::Array {
252                array: (Box::new(self.solana_type_to_idl_type(inner)), *size),
253            },
254            SolanaType::Vec(inner) => IdlType::Vec {
255                vec: Box::new(self.solana_type_to_idl_type(inner)),
256            },
257            SolanaType::Option(inner) => IdlType::Option {
258                option: Box::new(self.solana_type_to_idl_type(inner)),
259            },
260            SolanaType::Mapping(_, _) => IdlType::Primitive("bytes".to_string()), // Mappings are PDAs
261            SolanaType::Custom(name) => IdlType::Defined(name.clone()),
262        }
263    }
264}
265
266// IDL structure types
267#[derive(Serialize)]
268struct Idl {
269    version: String,
270    name: String,
271    instructions: Vec<IdlInstruction>,
272    accounts: Vec<IdlAccountDef>,
273    types: Vec<IdlTypeDef>,
274    events: Vec<IdlEvent>,
275    errors: Vec<IdlError>,
276    metadata: IdlMetadata,
277}
278
279#[derive(Serialize)]
280struct IdlMetadata {
281    address: String,
282}
283
284#[derive(Serialize)]
285struct IdlInstruction {
286    name: String,
287    accounts: Vec<IdlAccount>,
288    args: Vec<IdlField>,
289    #[serde(skip_serializing_if = "Option::is_none")]
290    returns: Option<IdlType>,
291}
292
293#[derive(Serialize)]
294struct IdlAccount {
295    name: String,
296    #[serde(rename = "isMut")]
297    is_mut: bool,
298    #[serde(rename = "isSigner")]
299    is_signer: bool,
300}
301
302#[derive(Serialize)]
303struct IdlField {
304    name: String,
305    #[serde(rename = "type")]
306    ty: IdlType,
307}
308
309#[derive(Serialize)]
310struct IdlAccountDef {
311    name: String,
312    #[serde(rename = "type")]
313    ty: IdlAccountType,
314}
315
316#[derive(Serialize)]
317struct IdlAccountType {
318    kind: String,
319    fields: Vec<IdlField>,
320}
321
322#[derive(Serialize)]
323struct IdlTypeDef {
324    name: String,
325    #[serde(rename = "type")]
326    ty: IdlTypeDefType,
327}
328
329#[derive(Serialize)]
330#[serde(untagged)]
331enum IdlTypeDefType {
332    Struct { fields: Vec<IdlField> },
333    Enum { variants: Vec<IdlEnumVariant> },
334}
335
336#[derive(Serialize)]
337struct IdlEnumVariant {
338    name: String,
339}
340
341#[derive(Serialize)]
342struct IdlEvent {
343    name: String,
344    fields: Vec<IdlEventField>,
345}
346
347#[derive(Serialize)]
348struct IdlEventField {
349    name: String,
350    #[serde(rename = "type")]
351    ty: IdlType,
352    index: bool,
353}
354
355#[derive(Serialize)]
356struct IdlError {
357    code: u32,
358    name: String,
359    msg: String,
360}
361
362#[derive(Serialize)]
363#[serde(untagged)]
364enum IdlType {
365    Primitive(String),
366    Defined(String),
367    Array { array: (Box<IdlType>, usize) },
368    Vec { vec: Box<IdlType> },
369    Option { option: Box<IdlType> },
370}
371
372// Helper functions
373fn to_camel_case(s: &str) -> String {
374    let mut result = String::new();
375    let mut capitalize_next = true;
376
377    for c in s.chars() {
378        if c == '_' || c == '-' {
379            capitalize_next = true;
380        } else if capitalize_next {
381            result.push(c.to_ascii_uppercase());
382            capitalize_next = false;
383        } else {
384            result.push(c);
385        }
386    }
387
388    result
389}
390
391fn to_camel_case_lower(s: &str) -> String {
392    let camel = to_camel_case(s);
393    let mut chars = camel.chars();
394    match chars.next() {
395        None => String::new(),
396        Some(c) => c.to_ascii_lowercase().to_string() + chars.as_str(),
397    }
398}
399
400fn to_snake_case(s: &str) -> String {
401    let mut result = String::new();
402    for (i, c) in s.chars().enumerate() {
403        if c.is_uppercase() && i > 0 {
404            result.push('_');
405            result.push(c.to_ascii_lowercase());
406        } else {
407            result.push(c.to_ascii_lowercase());
408        }
409    }
410    result
411}