anchor_client_gen_utils/
lib.rs

1use core::panic;
2use std::{env, path::PathBuf, str::FromStr};
3
4use proc_macro2::TokenStream;
5use quote::quote;
6use solana_sdk::pubkey::Pubkey;
7
8use crate::{
9    generator::{events, instructions, state},
10    idl::IdlJsonDefinition,
11    meta::Meta,
12};
13
14pub mod generator;
15pub mod idl;
16pub mod meta;
17
18#[derive(Default, PartialEq, Debug)]
19pub struct TypesAndAccountsConfig {
20    /// Accounts and types that should be zero_copy
21    /// Names separated by `,`.
22    pub zero_copy: Vec<String>,
23    /// Accounts and types that should be zero_copy(unsafe).
24    /// Should be used on zero_copy accounts if anchor version is <0.27.0.
25    /// Accounts in zero_copy_unsafe can not be specified in zero_copy
26    /// Names separated by `,`.
27    pub zero_copy_unsafe: Vec<String>,
28    /// Accounts and types that should have C compatible memory representation.
29    /// #[repr(C)] is default with zero_copy and zero_copy_unsafe.
30    /// Names separated by `,`.
31    pub repr_c: Vec<String>,
32    /// Accounts and types that should have memory layout without any padding.
33    /// One account can have both C and packed memory representation.
34    /// Names separated by `,`.
35    pub repr_packed: Vec<String>,
36}
37
38impl TypesAndAccountsConfig {
39    pub fn validate(&self) {
40        let mut duplicates = String::new();
41        for zero_copy in &self.zero_copy {
42            if self.zero_copy_unsafe.contains(zero_copy) {
43                duplicates.push_str(&format!("{}, ", zero_copy.clone()));
44            }
45        }
46
47        if duplicates.len() > 0 {
48            panic!(
49                "zero_copy and zero_copy_unsafe can not contain same identifiers. Duplicates: {}",
50                &duplicates[..duplicates.len() - 2]
51            )
52        }
53    }
54}
55
56#[derive(Default, PartialEq, Debug)]
57pub struct Args {
58    /// Path to <idl>.json
59    pub idl_path: PathBuf,
60    /// Program id
61    pub program_id: String,
62    /// Skip generation of Error enum
63    pub skip_errors: bool,
64    /// Skip generation of events
65    pub skip_events: bool,
66    pub types_and_accounts_config: TypesAndAccountsConfig,
67}
68
69impl Args {
70    fn remove_whitespace(str: &str) -> String {
71        str.chars()
72            .filter(|c| !c.is_whitespace())
73            .collect::<String>()
74    }
75
76    fn parse_inside_parenthesis<'a, T: Iterator<Item = &'a str>>(
77        current: &str,
78        args: &mut T,
79        target: &mut Vec<String>,
80        name: &str,
81    ) {
82        match current.split("(").collect::<Vec<&str>>()[..] {
83            [_, val] => {
84                if val.ends_with(")") {
85                    let val = &val[..val.len() - 1];
86                    target.push(val.to_owned());
87                    return;
88                } else {
89                    target.push(val.to_owned());
90                }
91            }
92            _ => panic!("Invalid {} arg", name),
93        }
94
95        while let Some(arg) = args.next() {
96            if arg.ends_with(")") {
97                let val = &arg[..arg.len() - 1];
98                target.push(val.to_owned());
99                return;
100            } else {
101                target.push(arg.to_owned());
102            };
103        }
104        panic!("Invalid {} arg", name);
105    }
106
107    pub fn parse(args: String) -> Self {
108        let args_sanitized = args.replace('\"', "").replace('\n', " ");
109        let mut args = args_sanitized.split(",").map(|arg| arg.trim());
110
111        let mut idl_path: Option<PathBuf> = None;
112        let mut program_id: Option<String> = None;
113        let mut types_and_accounts_config = TypesAndAccountsConfig::default();
114        let mut skip_errors = false;
115        let mut skip_events = false;
116
117        while let Some(arg) = args.next() {
118            if arg.starts_with("idl_path") {
119                match Self::remove_whitespace(arg)
120                    .split("=")
121                    .collect::<Vec<&str>>()[..]
122                {
123                    [_, path] => {
124                        if !path.ends_with(".json") {
125                            panic!("Idl file needs to be in JSON format")
126                        }
127                        let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
128                        idl_path = Some(
129                            PathBuf::from_str(&cargo_manifest_dir)
130                                .expect("Invalid idl_path arg")
131                                .join(path),
132                        );
133                    }
134                    _ => panic!("Invalid idl_path arg"),
135                }
136                continue;
137            }
138            if arg.starts_with("program_id") {
139                match Self::remove_whitespace(arg)
140                    .split("=")
141                    .collect::<Vec<&str>>()[..]
142                {
143                    [_, program_id_str] => {
144                        Pubkey::from_str(program_id_str)
145                            .expect("program_id is not valid public key");
146                        program_id = Some(program_id_str.to_owned());
147                    }
148                    _ => panic!("Invalid program_id arg"),
149                }
150                continue;
151            }
152
153            match arg {
154                "skip_errors" => {
155                    skip_errors = true;
156                }
157                "skip_events" => {
158                    skip_events = true;
159                }
160                _ => {
161                    if arg.starts_with("zero_copy_unsafe") {
162                        Self::parse_inside_parenthesis(
163                            arg,
164                            &mut args,
165                            &mut types_and_accounts_config.zero_copy_unsafe,
166                            "zero_copy_unsafe",
167                        );
168                    } else if arg.starts_with("zero_copy") {
169                        Self::parse_inside_parenthesis(
170                            arg,
171                            &mut args,
172                            &mut types_and_accounts_config.zero_copy,
173                            "zero_copy",
174                        );
175                    } else if arg.starts_with("repr_c") {
176                        Self::parse_inside_parenthesis(
177                            arg,
178                            &mut args,
179                            &mut types_and_accounts_config.repr_c,
180                            "repr_c",
181                        );
182                    } else if arg.starts_with("repr_packed") {
183                        Self::parse_inside_parenthesis(
184                            arg,
185                            &mut args,
186                            &mut types_and_accounts_config.repr_packed,
187                            "repr_packed",
188                        );
189                    } else {
190                        panic!("Invalid arg");
191                    }
192                }
193            }
194        }
195
196        if idl_path.is_none() {
197            panic!("Missing idl_path arg");
198        }
199
200        if program_id.is_none() {
201            panic!("Missing program_id arg");
202        }
203
204        types_and_accounts_config.validate();
205
206        Self {
207            program_id: program_id.unwrap(),
208            idl_path: idl_path.unwrap(),
209            skip_errors,
210            skip_events,
211            types_and_accounts_config,
212        }
213    }
214}
215
216pub fn generate(args: Args) -> TokenStream {
217    let idl = &IdlJsonDefinition::read_idl(&args.idl_path);
218    let meta = Meta::from(idl);
219    let program_id = args.program_id;
220
221    let types = if idl.types.len() > 0 {
222        let types = state::generate(
223            idl,
224            &idl.types,
225            &args.types_and_accounts_config,
226            &meta,
227            false,
228        );
229        quote! {
230            pub mod types {
231                #types
232            }
233        }
234    } else {
235        quote! {}
236    };
237    let accounts = if idl.accounts.len() > 0 {
238        let accounts = state::generate(
239            idl,
240            &idl.accounts,
241            &args.types_and_accounts_config,
242            &meta,
243            true,
244        );
245        quote! {
246            pub mod accounts {
247                #accounts
248            }
249        }
250    } else {
251        quote! {}
252    };
253    let instructions = if idl.instructions.len() > 0 {
254        let instructions = instructions::generate(idl);
255        quote! {
256            pub mod instructions {
257                #instructions
258            }
259        }
260    } else {
261        quote! {}
262    };
263
264    let events = if !args.skip_events && idl.events.len() > 0 {
265        let events = events::generate(idl, &meta);
266        quote! {
267            #events
268        }
269    } else {
270        quote! {}
271    };
272
273    quote! {
274        anchor_lang::declare_id!(#program_id);
275
276        #types
277
278        #accounts
279
280        #instructions
281
282        #events
283    }
284}
285
286#[cfg(test)]
287mod tests {
288    use std::{env, path::PathBuf};
289
290    use crate::{Args, TypesAndAccountsConfig};
291
292    #[test]
293    fn parse_args() {
294        let args = "idl_path = \"idl.json\", program_id =\n\"NativeLoader1111111111111111111111111111111\", skip_errors,\nzero_copy_unsafe(PerpMarket, Amm, PoolBalance, InsuranceClaim),\nrepr_c(PerpMarket)".to_owned();
295        let cargo_manifest_dir = env::var("CARGO_MANIFEST_DIR").unwrap();
296        let parsed = Args::parse(args);
297        let should_be = Args {
298            idl_path: PathBuf::from(cargo_manifest_dir).join("idl.json"),
299            program_id: "NativeLoader1111111111111111111111111111111".to_owned(),
300            skip_errors: true,
301            skip_events: false,
302            types_and_accounts_config: TypesAndAccountsConfig {
303                zero_copy: vec![],
304                zero_copy_unsafe: vec![
305                    "PerpMarket".to_owned(),
306                    "Amm".to_owned(),
307                    "PoolBalance".to_owned(),
308                    "InsuranceClaim".to_owned(),
309                ],
310                repr_c: vec!["PerpMarket".to_owned()],
311                repr_packed: vec![],
312            },
313        };
314
315        assert_eq!(parsed, should_be);
316    }
317
318    #[test]
319    #[should_panic]
320    fn parse_args_panic_1() {
321        Args::parse("idl_path = \"idl.json\",\nzero_copy_unsafe(PerpMarket,)".to_owned());
322    }
323
324    #[test]
325    #[should_panic]
326    fn parse_args_panic_2() {
327        Args::parse("idl_path = \"idl.json\",\nzero_copy_unsafe(PerpMarket)".to_owned());
328    }
329
330    #[test]
331    #[should_panic]
332    fn parse_args_panic_duplicates() {
333        Args::parse(
334            "idl_path = idl.json,\nzero_copy(PerpMarket),\nzero_copy_unsafe(PerpMarket), program_id =\n\"NativeLoader1111111111111111111111111111111\"".to_string(),
335        );
336    }
337}