kaptn_derive_extrametas/
lib.rs1use proc_macro::TokenStream;
2use quote::{quote, ToTokens};
3use syn::{
4 parse::{Parse, ParseStream},
5 parse_macro_input,
6 punctuated::Punctuated,
7 Attribute, Data, DeriveInput, Expr, Fields, GenericParam, Lit, Meta, NestedMeta, Token,
8};
9
10struct Seeds(Punctuated<Expr, Token![,]>);
11
12impl Parse for Seeds {
13 fn parse(input: ParseStream) -> syn::Result<Self> {
14 let content;
15 syn::bracketed!(content in input);
16 Ok(Seeds(content.parse_terminated(Expr::parse)?))
17 }
18}
19
20#[proc_macro_derive(ExtraMetas, attributes(meta))]
21pub fn derive_extra_metas(input: TokenStream) -> TokenStream {
22 let input = parse_macro_input!(input as DeriveInput);
23 let name = &input.ident;
24
25 let lifetimes: Vec<_> = input
27 .generics
28 .params
29 .iter()
30 .filter_map(|param| {
31 if let GenericParam::Lifetime(lt) = param {
32 Some(lt.lifetime.clone())
33 } else {
34 None
35 }
36 })
37 .collect();
38
39 let has_info_lifetime = lifetimes.iter().any(|lt| lt.to_string() == "'info");
41
42 let fields = if let Data::Struct(data_struct) = &input.data {
44 if let Fields::Named(fields) = &data_struct.fields {
45 fields.named.iter().collect::<Vec<_>>()
46 } else {
47 vec![]
48 }
49 } else {
50 vec![]
51 };
52
53 let account_metas = fields
55 .iter()
56 .filter_map(|f| {
57 let ident = f.ident.as_ref()?;
58 let meta_attr = f.attrs.iter().find(|attr| attr.path.is_ident("meta"))?;
59 parse_meta_attribute(ident, meta_attr).ok()
60 })
61 .collect::<Vec<_>>();
62
63 let field_names = fields
64 .iter()
65 .filter_map(|f| f.ident.as_ref())
66 .collect::<Vec<_>>();
67
68 let from_accounts = if has_info_lifetime {
70 quote! {
71 fn from_accounts(accounts: &[AccountInfo<'info>]) -> Result<Self, ProgramError> {
72 let mut iter = accounts.iter().skip(5); Ok(Self {
74 #(#field_names: iter.next().ok_or(ProgramError::NotEnoughAccountKeys)?.clone(),)*
75 })
76 }
77 }
78 } else {
79 quote! {
80 fn from_accounts(accounts: &[AccountInfo]) -> Result<Self, ProgramError> {
81 let mut iter = accounts.iter().skip(5); Ok(Self {
83 #(#field_names: iter.next().ok_or(ProgramError::NotEnoughAccountKeys)?.clone(),)*
84 })
85 }
86 }
87 };
88
89 let gen = if has_info_lifetime {
91 quote! {
92 impl<#(#lifetimes),*> ExtraMetas<#(#lifetimes),*> for #name<#(#lifetimes),*> {
93 #from_accounts
94
95 fn to_extra_account_metas() -> Vec<ExtraAccountMeta> {
96 vec![
97 #(#account_metas),*
98 ]
99 }
100 }
101 }
102 } else if !lifetimes.is_empty() {
103 quote! {
104 impl<#(#lifetimes),*> ExtraMetas<'_, #(#lifetimes),*> for #name<#(#lifetimes),*> {
105 #from_accounts
106
107 fn to_extra_account_metas() -> Vec<ExtraAccountMeta> {
108 vec![
109 #(#account_metas),*
110 ]
111 }
112 }
113 }
114 } else {
115 quote! {
116 impl ExtraMetas<'_> for #name {
117 #from_accounts
118
119 fn to_extra_account_metas() -> Vec<ExtraAccountMeta> {
120 vec![
121 #(#account_metas),*
122 ]
123 }
124 }
125 }
126 };
127
128 gen.into()
129}
130
131fn parse_meta_attribute(
132 _ident: &syn::Ident,
133 attr: &Attribute,
134) -> syn::Result<proc_macro2::TokenStream> {
135 let meta = attr.parse_meta()?;
136 if let Meta::List(list) = meta {
137 let mut pubkey = None;
138 let mut signer = false;
139 let mut writable = false;
140 let mut seeds = None;
141
142 for nested in list.nested.iter() {
143 match nested {
144 NestedMeta::Meta(Meta::NameValue(nv)) => {
145 match nv.path.get_ident().map(|i| i.to_string()).as_deref() {
146 Some("pubkey") => {
147 if let Lit::Str(lit) = &nv.lit {
148 pubkey = Some(lit.value());
149 }
150 }
151 Some("signer") => {
152 if let Lit::Bool(lit) = &nv.lit {
153 signer = lit.value();
154 }
155 }
156 Some("writable") => {
157 if let Lit::Bool(lit) = &nv.lit {
158 writable = lit.value();
159 }
160 }
161 _ => {}
162 }
163 }
164 NestedMeta::Meta(Meta::List(list)) if list.path.is_ident("seeds") => {
165 seeds = Some(syn::parse2::<Seeds>(list.nested.to_token_stream())?);
166 }
167 _ => {}
168 }
169 }
170
171 if pubkey.is_some() && seeds.is_some() {
172 return Err(syn::Error::new_spanned(
173 attr,
174 "Cannot specify both pubkey and seeds",
175 ));
176 }
177
178 if pubkey.is_none() && seeds.is_none() {
179 return Err(syn::Error::new_spanned(
180 attr,
181 "Must specify either pubkey or seeds",
182 ));
183 }
184
185 Ok(if let Some(pk) = pubkey {
187 quote! {
188 ExtraAccountMeta::new_with_pubkey(&#pk.parse().unwrap(), #signer, #writable).unwrap()
189 }
190 } else if let Some(Seeds(seeds)) = seeds {
191 let seed_exprs = seeds.iter();
192 quote! {
193 ExtraAccountMeta::new_external_pda_with_seeds(
194 0, &[#(#seed_exprs),*],
196 #signer,
197 #writable
198 ).unwrap()
199 }
200 } else {
201 unreachable!()
202 })
203 } else {
204 Err(syn::Error::new_spanned(
205 attr,
206 "Expected list-style attribute",
207 ))
208 }
209}