trdelnik_sandbox_client/
idl.rs

1//! The `idl` module contains structs and functions for Anchor program code parsing.
2//!
3//! [Idl] example:
4//!
5//! ```rust,ignore
6//! Idl {
7//!     programs: [
8//!         IdlProgram {
9//!             name: IdlName {
10//!                 snake_case: "turnstile",
11//!                 upper_camel_case: "Turnstile",
12//!             },
13//!             id: "[216u8 , 55u8 , 200u8 , 93u8 , 189u8 , 81u8 , 94u8 , 109u8 , 14u8 , 249u8 , 244u8 , 106u8 , 68u8 , 214u8 , 222u8 , 190u8 , 9u8 , 25u8 , 199u8 , 75u8 , 79u8 , 230u8 , 94u8 , 137u8 , 51u8 , 187u8 , 193u8 , 48u8 , 87u8 , 222u8 , 175u8 , 163u8]",
14//!             instruction_account_pairs: [
15//!                 (
16//!                     IdlInstruction {
17//!                         name: IdlName {
18//!                             snake_case: "initialize",
19//!                             upper_camel_case: "Initialize",
20//!                         },
21//!                         parameters: [],
22//!                     },
23//!                     IdlAccountGroup {
24//!                         name: IdlName {
25//!                             snake_case: "initialize",
26//!                             upper_camel_case: "Initialize",
27//!                         },
28//!                         accounts: [
29//!                             (
30//!                                 "state",
31//!                                 "anchor_lang :: solana_program :: pubkey :: Pubkey",
32//!                             ),
33//!                             (
34//!                                 "user",
35//!                                 "anchor_lang :: solana_program :: pubkey :: Pubkey",
36//!                             ),
37//!                             (
38//!                                 "system_program",
39//!                                 "anchor_lang :: solana_program :: pubkey :: Pubkey",
40//!                             ),
41//!                         ],
42//!                     },
43//!                 ),
44//!                 (
45//!                     IdlInstruction {
46//!                         name: IdlName {
47//!                             snake_case: "coin",
48//!                             upper_camel_case: "Coin",
49//!                         },
50//!                         parameters: [
51//!                             (
52//!                                 "dummy_arg",
53//!                                 "String",
54//!                             ),
55//!                         ],
56//!                     },
57//!                     IdlAccountGroup {
58//!                         name: IdlName {
59//!                             snake_case: "update_state",
60//!                             upper_camel_case: "UpdateState",
61//!                         },
62//!                         accounts: [
63//!                             (
64//!                                 "state",
65//!                                 "anchor_lang :: solana_program :: pubkey :: Pubkey",
66//!                             ),
67//!                         ],
68//!                     },
69//!                 ),
70//!                 (
71//!                     IdlInstruction {
72//!                         name: IdlName {
73//!                             snake_case: "push",
74//!                             upper_camel_case: "Push",
75//!                         },
76//!                         parameters: [],
77//!                     },
78//!                     IdlAccountGroup {
79//!                         name: IdlName {
80//!                             snake_case: "update_state",
81//!                             upper_camel_case: "UpdateState",
82//!                         },
83//!                         accounts: [
84//!                             (
85//!                                 "state",
86//!                                 "anchor_lang :: solana_program :: pubkey :: Pubkey",
87//!                             ),
88//!                         ],
89//!                     },
90//!                 ),
91//!             ],
92//!         },
93//!     ],
94//! }
95//! ```
96
97use heck::{ToSnakeCase, ToUpperCamelCase};
98use quote::ToTokens;
99use thiserror::Error;
100
101static ACCOUNT_MOD_PREFIX: &str = "__client_accounts_";
102
103#[derive(Error, Debug)]
104pub enum Error {
105    #[error("{0:?}")]
106    RustParsingError(#[from] syn::Error),
107    #[error("missing or invalid program item: '{0}'")]
108    MissingOrInvalidProgramItems(&'static str),
109}
110
111#[derive(Debug)]
112pub struct Idl {
113    pub programs: Vec<IdlProgram>,
114}
115
116#[derive(Debug)]
117pub struct IdlName {
118    pub snake_case: String,
119    pub upper_camel_case: String,
120}
121
122#[derive(Debug)]
123pub struct IdlProgram {
124    pub name: IdlName,
125    pub id: String,
126    pub instruction_account_pairs: Vec<(IdlInstruction, IdlAccountGroup)>,
127}
128
129#[derive(Debug)]
130pub struct IdlInstruction {
131    pub name: IdlName,
132    pub parameters: Vec<(String, String)>,
133}
134
135#[derive(Debug)]
136pub struct IdlAccountGroup {
137    pub name: IdlName,
138    pub accounts: Vec<(String, String)>,
139}
140
141pub async fn parse_to_idl_program(name: String, code: &str) -> Result<IdlProgram, Error> {
142    let mut static_program_id = None::<syn::ItemStatic>;
143    let mut mod_private = None::<syn::ItemMod>;
144    let mut mod_instruction = None::<syn::ItemMod>;
145    let mut account_mods = Vec::<syn::ItemMod>::new();
146
147    for item in syn::parse_file(code)?.items.into_iter() {
148        match item {
149            syn::Item::Static(item_static) if item_static.ident == "ID" => {
150                static_program_id = Some(item_static);
151            }
152            syn::Item::Mod(item_mod) => match item_mod.ident.to_string().as_str() {
153                "__private" => mod_private = Some(item_mod),
154                "instruction" => mod_instruction = Some(item_mod),
155                _ => set_account_modules(&mut account_mods, item_mod),
156            },
157            _ => (),
158        }
159    }
160
161    let static_program_id =
162        static_program_id.ok_or(Error::MissingOrInvalidProgramItems("missing static ID"))?;
163    let mod_private =
164        mod_private.ok_or(Error::MissingOrInvalidProgramItems("missing mod private"))?;
165    let mod_instruction = mod_instruction.ok_or(Error::MissingOrInvalidProgramItems(
166        "missing mod instruction",
167    ))?;
168
169    // ------ get program id ------
170
171    // input example:
172    // ```
173    // pub static ID: anchor_lang::solana_program::pubkey::Pubkey =
174    //     anchor_lang::solana_program::pubkey::Pubkey::new_from_array([216u8, 55u8,
175    //                                                                  200u8, 93u8,
176    //                                                                  189u8, 81u8,
177    //                                                                  94u8, 109u8,
178    //                                                                  14u8, 249u8,
179    //                                                                  244u8, 106u8,
180    //                                                                  68u8, 214u8,
181    //                                                                  222u8, 190u8,
182    //                                                                  9u8, 25u8,
183    //                                                                  199u8, 75u8,
184    //                                                                  79u8, 230u8,
185    //                                                                  94u8, 137u8,
186    //                                                                  51u8, 187u8,
187    //                                                                  193u8, 48u8,
188    //                                                                  87u8, 222u8,
189    //                                                                  175u8,
190    //                                                                  163u8]);
191    // ```
192
193    let program_id_bytes = {
194        let new_pubkey_call = match *static_program_id.expr {
195            syn::Expr::Call(new_pubkey_call) => new_pubkey_call,
196            _ => {
197                return Err(Error::MissingOrInvalidProgramItems(
198                    "static ID: new pubkey call not found",
199                ))
200            }
201        };
202        match new_pubkey_call.args.into_iter().next() {
203            Some(syn::Expr::Array(pubkey_bytes)) => pubkey_bytes,
204            _ => {
205                return Err(Error::MissingOrInvalidProgramItems(
206                    "static ID: pubkey bytes not found",
207                ))
208            }
209        }
210    };
211
212    // ------ get instruction_item_fns ------
213
214    // input example:
215    // ```
216    // mod __private {
217    //     pub mod __global {
218    //         use super::*;
219    //         #[inline(never)]
220    //         pub fn initialize(program_id: &Pubkey, accounts: &[AccountInfo],
221    //                           ix_data: &[u8]) -> ProgramResult {
222    //             let ix =
223    //                 instruction::Initialize::deserialize(&mut &ix_data[..]).map_err(|_|
224    //                                                                                     anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?;
225    //             let instruction::Initialize = ix;
226    //             let mut remaining_accounts: &[AccountInfo] = accounts;
227    //             let mut accounts =
228    //                 Initialize::try_accounts(program_id, &mut remaining_accounts,
229    //                                          ix_data)?;
230    //             turnstile::initialize(Context::new(program_id, &mut accounts,
231    //                                                remaining_accounts))?;
232    //             accounts.exit(program_id)
233    //         }
234    // ```
235
236    let instruction_item_fns = {
237        let items = mod_private
238            .content
239            .map(|(_, items)| items)
240            .unwrap_or_default();
241        let item_mod_global = items
242            .into_iter()
243            .find_map(|item| match item {
244                syn::Item::Mod(item_mod) if item_mod.ident == "__global" => Some(item_mod),
245                _ => None?,
246            })
247            .ok_or(Error::MissingOrInvalidProgramItems(
248                "mod private: mod global not found",
249            ))?;
250        let items = item_mod_global
251            .content
252            .map(|(_, items)| items)
253            .unwrap_or_default();
254        items.into_iter().filter_map(|item| match item {
255            syn::Item::Fn(item_fn) => Some(item_fn),
256            _ => None,
257        })
258    };
259
260    // ------ get instruction + account group names ------
261
262    // input example:
263    // ```
264    //         pub fn initialize(program_id: &Pubkey, accounts: &[AccountInfo],
265    //                           ix_data: &[u8]) -> ProgramResult {
266    //             let ix =
267    //                 instruction::Initialize::deserialize(&mut &ix_data[..]).map_err(|_|
268    //                                                                                     anchor_lang::__private::ErrorCode::InstructionDidNotDeserialize)?;
269    //             let instruction::Initialize = ix;
270    //             let mut remaining_accounts: &[AccountInfo] = accounts;
271    //             let mut accounts =
272    //                 Initialize::try_accounts(program_id, &mut remaining_accounts,
273    //                                          ix_data)?;
274    //             turnstile::initialize(Context::new(program_id, &mut accounts,
275    //                                                remaining_accounts))?;
276    //             accounts.exit(program_id)
277    //         }
278    // ```
279
280    let mut instruction_account_pairs = Vec::new();
281    instruction_item_fns
282        .into_iter()
283        .map(|item_fn| {
284            // stmt example: `let mut accounts = UpdateState::try_accounts(program_id, &mut remaining_accounts, ix_data)?;`
285            let account_group_name = item_fn.block.stmts.into_iter().find_map(|stmt| {
286            let local = if let syn::Stmt::Local(local) = stmt {
287                local
288            } else {
289                None?
290            };
291            if !matches!(&local.pat, syn::Pat::Ident(pat_ident) if pat_ident.ident == "accounts") {
292                None?
293            }
294            let init_expr = *local.init?.1;
295            let expr_try_expr = match init_expr {
296                syn::Expr::Try(expr_try) => *expr_try.expr,
297                _ => None?
298            };
299            let expr_call_func = match expr_try_expr {
300                syn::Expr::Call(expr_call) => *expr_call.func,
301                _ => None?
302            };
303            let account_group_name = match expr_call_func {
304                syn::Expr::Path(expr_path) => expr_path.path.segments.into_iter().next()?.ident,
305                _ => None?
306            };
307            Some(account_group_name.to_string())
308        })?;
309
310            let instruction_name = item_fn.sig.ident.to_string();
311            let idl_instruction = IdlInstruction {
312                name: IdlName {
313                    upper_camel_case: instruction_name.to_upper_camel_case(),
314                    snake_case: instruction_name,
315                },
316                parameters: Vec::new(),
317            };
318            let idl_account = IdlAccountGroup {
319                name: IdlName {
320                    snake_case: account_group_name.to_snake_case(),
321                    upper_camel_case: account_group_name,
322                },
323                accounts: Vec::new(),
324            };
325            Some((idl_instruction, idl_account))
326        })
327        .try_for_each(|pair| {
328            if let Some(pair) = pair {
329                instruction_account_pairs.push(pair);
330                Ok(())
331            } else {
332                Err(Error::MissingOrInvalidProgramItems(
333                    "statement with `accounts` not found",
334                ))
335            }
336        })?;
337
338    // ------ get instruction parameters ------
339
340    // input example:
341    // ```
342    // pub mod instruction {
343    //     use super::*;
344    //     pub mod state {
345    //         use super::*;
346    //     }
347    // // **
348    //     pub struct Initialize;
349    // // **
350    //     pub struct Coin {
351    //         pub dummy_arg: String,
352    //     }
353    // ```
354
355    let mut instruction_mod_items = mod_instruction
356        .content
357        .ok_or(Error::MissingOrInvalidProgramItems(
358            "instruction mod: empty content",
359        ))?
360        .1
361        .into_iter();
362
363    for (idl_instruction, _) in &mut instruction_account_pairs {
364        let instruction_struct_name = &idl_instruction.name.upper_camel_case;
365
366        let instruction_item_struct_fields = instruction_mod_items
367            .find_map(|item| {
368                let instruction_item_struct = match item {
369                    syn::Item::Struct(item_struct)
370                        if item_struct.ident == instruction_struct_name =>
371                    {
372                        item_struct
373                    }
374                    _ => None?,
375                };
376                let fields = match instruction_item_struct.fields {
377                    syn::Fields::Named(fields_named) => fields_named.named,
378                    syn::Fields::Unit => syn::punctuated::Punctuated::new(),
379                    syn::Fields::Unnamed(_) => None?,
380                };
381                Some(fields.into_iter())
382            })
383            .ok_or(Error::MissingOrInvalidProgramItems("instruction struct"))?;
384
385        idl_instruction.parameters = instruction_item_struct_fields
386            .map(|field| {
387                let parameter_name = field.ident.unwrap().to_string();
388                let parameter_id_type = field.ty.into_token_stream().to_string();
389                (parameter_name, parameter_id_type)
390            })
391            .collect();
392    }
393
394    // ------ get accounts ------
395
396    // input example:
397    // ```
398    // pub(crate) mod __client_accounts_initialize {
399    //     use super::*;
400    //     use anchor_lang::prelude::borsh;
401    //     pub struct Initialize {
402    //         pub state: anchor_lang::solana_program::pubkey::Pubkey,
403    //         pub user: anchor_lang::solana_program::pubkey::Pubkey,
404    //         pub system_program: anchor_lang::solana_program::pubkey::Pubkey,
405    //     }
406    // ```
407
408    for account_mod_item in account_mods {
409        let account_struct_name = account_mod_item
410            .ident
411            .to_string()
412            .strip_prefix(ACCOUNT_MOD_PREFIX)
413            .unwrap()
414            .to_upper_camel_case();
415
416        let account_item_struct = account_mod_item
417            .content
418            .ok_or(Error::MissingOrInvalidProgramItems(
419                "account mod: empty content",
420            ))?
421            .1
422            .into_iter()
423            .find_map(|item| match item {
424                syn::Item::Struct(item_struct) if item_struct.ident == account_struct_name => {
425                    Some(item_struct)
426                }
427                _ => None?,
428            })
429            .ok_or(Error::MissingOrInvalidProgramItems(
430                "account mod: struct not found",
431            ))?;
432
433        let account_item_struct_fields = match account_item_struct.fields {
434            syn::Fields::Named(fields_named) => fields_named.named,
435            syn::Fields::Unit => syn::punctuated::Punctuated::new(),
436            syn::Fields::Unnamed(_) => {
437                return Err(Error::MissingOrInvalidProgramItems(
438                    "account struct: unnamed fields not allowed",
439                ))
440            }
441        };
442
443        let accounts = account_item_struct_fields
444            .into_iter()
445            .map(|field| {
446                let account_name = field.ident.unwrap().to_string();
447                let account_id_type = field.ty.into_token_stream().to_string();
448                (account_name, account_id_type)
449            })
450            .collect::<Vec<_>>();
451
452        for (_, idl_account_group) in &mut instruction_account_pairs {
453            if idl_account_group.name.upper_camel_case == account_struct_name {
454                idl_account_group.accounts = accounts.clone();
455            }
456        }
457    }
458
459    // ------ // ------
460
461    Ok(IdlProgram {
462        name: IdlName {
463            upper_camel_case: name.to_upper_camel_case(),
464            snake_case: name,
465        },
466        id: program_id_bytes.into_token_stream().to_string(),
467        instruction_account_pairs,
468    })
469}
470
471fn set_account_modules(account_modules: &mut Vec<syn::ItemMod>, item_module: syn::ItemMod) {
472    if item_module
473        .ident
474        .to_string()
475        .starts_with(ACCOUNT_MOD_PREFIX)
476    {
477        account_modules.push(item_module);
478        return;
479    }
480    let modules = item_module
481        .content
482        .ok_or(Error::MissingOrInvalidProgramItems(
483            "account mod: empty content",
484        ))
485        .unwrap()
486        .1;
487    for module in modules {
488        if let syn::Item::Mod(nested_module) = module {
489            set_account_modules(account_modules, nested_module);
490        }
491    }
492}