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}