pda_buddy/lib.rs
1//! # pda-buddy
2//!
3//! `pda-buddy` is a small Anchor helper for keeping PDA seed definitions in one
4//! place.
5//!
6//! Anchor already has a good PDA model. This crate does not replace it.
7//! Instead, it lets you define the seed ABI once on the PDA account type, then
8//! reuse that definition in:
9//!
10//! - `#[account(...)]` constraints
11//! - CPI signer seed arrays
12//! - optional helper methods for finding or asserting PDAs
13//!
14//! The generated account constraints are normal Anchor-compatible
15//! `seeds = [...]` and `bump` constraints, so IDL account resolution still works
16//! the way Anchor expects.
17//!
18//! ## Runtime Cost
19//!
20//! `pda-buddy` is a proc-macro crate. It does not add an on-chain runtime
21//! dependency or a second PDA validation layer to your program.
22//!
23//! Account bindings expand to Anchor's native `seeds = [...]` and `bump`
24//! constraints. Signer bindings expand to local stack values and a normal
25//! `&[&[&[u8]]]` signer seed slice. In a normal optimized deployment, this
26//! should have the same CU and program-size profile as writing the equivalent
27//! Anchor code by hand.
28//!
29//! The extra strict seed checks are Rust type checks generated at compile time.
30//! They are emitted as no-op Anchor constraints that evaluate to `true`, so
31//! optimized builds should remove them. Unchecked bindings with `!` skip those
32//! strict checks entirely.
33//!
34//! ## Quick Example
35//!
36//! Define the PDA seed ABI on the account type:
37//!
38//! ```ignore
39//! use anchor_lang::prelude::*;
40//! use pda_buddy::pda;
41//!
42//! #[account]
43//! #[pda(seeds = [
44//! prefix = b"message",
45//! from: Pubkey,
46//! to: Pubkey,
47//! id: u64,
48//! ])]
49//! pub struct Message {
50//! pub id: u64,
51//! }
52//! ```
53//!
54//! Use that ABI in an Anchor accounts context:
55//!
56//! ```ignore
57//! use pda_buddy::pda_accounts;
58//!
59//! #[pda_accounts]
60//! #[derive(Accounts)]
61//! #[instruction(params: Params)]
62//! pub struct Initialize<'info> {
63//! #[account(
64//! init,
65//! payer = payer,
66//! space = 8 + core::mem::size_of::<Message>(),
67//! pda(Message, [
68//! prefix,
69//! from = payer.key(),
70//! to = params.to,
71//! id = params.id,
72//! ])
73//! )]
74//! pub message: Account<'info, Message>,
75//!
76//! #[account(mut)]
77//! pub payer: Signer<'info>,
78//!
79//! pub system_program: Program<'info, System>,
80//! }
81//! ```
82//!
83//! `pda-buddy` expands the `pda(...)` binding into normal Anchor constraints:
84//!
85//! ```ignore
86//! seeds = [
87//! b"message",
88//! payer.key().as_ref(),
89//! params.to.as_ref(),
90//! params.id.to_le_bytes().as_ref(),
91//! ],
92//! bump
93//! ```
94//!
95//! Bindings may be written in any order. The final generated seed order always
96//! follows the original `#[pda(seeds = [...])]` definition.
97//!
98//! ## Account Constraints
99//!
100//! Put `#[pda_accounts]` above `#[derive(Accounts)]`:
101//!
102//! ```ignore
103//! #[pda_accounts]
104//! #[derive(Accounts)]
105//! pub struct MyCtx<'info> {
106//! // fields
107//! }
108//! ```
109//!
110//! Inside an Anchor `#[account(...)]` attribute, add a top-level
111//! `pda(Type, [...])` item:
112//!
113//! ```ignore
114//! #[account(
115//! mut,
116//! pda(Message, [
117//! prefix,
118//! from = user.key(),
119//! to = params.to,
120//! id = params.id,
121//! ])
122//! )]
123//! pub message: Account<'info, Message>,
124//! ```
125//!
126//! Static seed bindings are written as just the label:
127//!
128//! ```ignore
129//! prefix
130//! ```
131//!
132//! Dynamic seed bindings are written as `label = expression`:
133//!
134//! ```ignore
135//! from = user.key()
136//! id = params.id
137//! ```
138//!
139//! For `Pubkey` seeds, pass a `Pubkey` expression. If the seed value comes from
140//! an Anchor account, call `.key()` explicitly:
141//!
142//! ```ignore
143//! from = payer.key()
144//! to = params.to
145//! ```
146//!
147//! If you intentionally want to bypass account or signer seed type checks for
148//! one binding, append `!` to the value. The generated seed expression is still
149//! emitted normally, so the value must still support the generated seed
150//! conversion such as `.as_ref()`:
151//!
152//! ```ignore
153//! user = "custom-bytes"!
154//! ```
155//!
156//! ## CPI Signer Seeds
157//!
158//! Use [`pda_signer!`] when a PDA signs a CPI:
159//!
160//! ```ignore
161//! use pda_buddy::pda_signer;
162//!
163//! pda_signer!(
164//! let message_signer = Message,
165//! bump = ctx.bumps.message,
166//! [
167//! prefix,
168//! from = ctx.accounts.payer.key(),
169//! to = params.to,
170//! id = params.id,
171//! ]
172//! );
173//!
174//! some_cpi_context.with_signer(message_signer);
175//! ```
176//!
177//! The macro creates the temporary byte arrays needed for numeric seeds and bump
178//! bytes in the caller scope. This keeps signer seeds lifetime-safe without heap
179//! allocation.
180//!
181//! Signer seed bindings also support the unchecked marker:
182//!
183//! ```ignore
184//! from = "wrong-but-i-want-this"!
185//! ```
186//!
187//! ## Defining Seeds
188//!
189//! Seeds are declared in `#[pda(seeds = [...])]`.
190//!
191//! ```ignore
192//! #[pda(seeds = [
193//! prefix = b"message",
194//! authority: Pubkey,
195//! slug: String,
196//! index: u64,
197//! shard: u16(be),
198//! active: bool,
199//! digest: [u8; 32],
200//! ])]
201//! pub struct Message {
202//! // account fields
203//! }
204//! ```
205//!
206//! Static seeds use `=`.
207//!
208//! ```ignore
209//! prefix = b"message"
210//! ```
211//!
212//! Static seeds must be byte-string literals or resolvable byte-string consts:
213//!
214//! ```ignore
215//! impl Message {
216//! pub const PREFIX: &'static [u8] = b"message";
217//! }
218//!
219//! #[pda(seeds = [
220//! prefix = Self::PREFIX,
221//! id: u64,
222//! ])]
223//! pub struct Message {
224//! pub id: u64,
225//! }
226//! ```
227//!
228//! Dynamic seeds use `:`.
229//!
230//! ```ignore
231//! authority: Pubkey
232//! id: u64
233//! ```
234//!
235//! Supported dynamic seed types:
236//!
237//! | Seed type | Encoding |
238//! | --- | --- |
239//! | `Pubkey` | `pubkey.as_ref()` |
240//! | `bytes`, `Bytes`, `[u8]`, `&[u8]`, `Vec<u8>` | `as_ref()` |
241//! | `str`, `String` | `as_bytes()` |
242//! | `bool` | one byte, `0` or `1` |
243//! | `u8`, `i8` | one byte |
244//! | `u16`, `u32`, `u64`, `i16`, `i32`, `i64` | little-endian by default |
245//! | `u16(be)`, `u32(be)`, `u64(be)`, `i16(be)`, `i32(be)`, `i64(be)` | big-endian |
246//! | `[u8; N]` | fixed byte array |
247//!
248//! `u64` and `u64(le)` are equivalent. `be` is explicit big-endian.
249//!
250//! ## Program ID
251//!
252//! By default, generated helper methods use `crate::ID` as the program id.
253//!
254//! If the PDA belongs to another program, set `program_id`:
255//!
256//! ```ignore
257//! #[pda(
258//! seeds = [
259//! prefix = b"vault",
260//! owner: Pubkey,
261//! ],
262//! program_id = other_program::ID,
263//! )]
264//! pub struct Vault {
265//! pub owner: Pubkey,
266//! }
267//! ```
268//!
269//! The account-constraint rewrite still emits Anchor `seeds` and `bump`
270//! constraints. Use Anchor's native `seeds::program = ...` support separately
271//! if you need cross-program PDA validation in an accounts context.
272//!
273//! ## Helper Methods
274//!
275//! `#[pda(...)]` can generate helper methods:
276//!
277//! ```ignore
278//! #[pda(
279//! seeds = [
280//! prefix = b"message",
281//! id: u64,
282//! ],
283//! features = [find, assert],
284//! )]
285//! pub struct Message {
286//! pub id: u64,
287//! }
288//! ```
289//!
290//! `find` generates:
291//!
292//! ```ignore
293//! let (address, bump) = Message::find_pda(id, None);
294//! ```
295//!
296//! `assert` generates:
297//!
298//! ```ignore
299//! let bump = Message::assert_pda(&address, id, None)?;
300//! ```
301//!
302//! The final argument is an optional program id override:
303//!
304//! ```ignore
305//! let (address, bump) = Message::find_pda(id, Some(&custom_program_id));
306//! ```
307//!
308//! ## Diagnostics
309//!
310//! The macros validate seed labels and binding kinds. If a label is missing,
311//! duplicated, unknown, or bound in the wrong form, the compiler error includes
312//! the original PDA seed definition and the expected binding shape.
313//!
314//! For example, if `from: Pubkey` is accidentally written as just `from`, the
315//! error points out that the expected binding is:
316//!
317//! ```text
318//! from = <Pubkey expression>
319//! ```
320//!
321//! ## Notes
322//!
323//! - `#[pda_accounts]` must appear above `#[derive(Accounts)]`.
324//! - `pda(...)` inside `#[account(...)]` must be a top-level account constraint item.
325//! - Static seed labels are bound by label only. Dynamic seed labels need `label = value`.
326//! - Complex runtime expressions are allowed, but Anchor may not include them in IDL PDA metadata. Simple paths, fields, and common Anchor calls like `.key()` are the most IDL-friendly.
327//! - This crate scans the consumer crate's `src/**/*.rs` files to find `#[pda(...)]` definitions. Keep PDA account definitions in normal source files under `src`.
328//!
329use proc_macro::TokenStream;
330use syn::{ItemStruct, parse_macro_input};
331
332use crate::pda::PdaArgs;
333
334mod accounts;
335mod binding;
336mod pda;
337mod registry;
338mod signer;
339mod spec;
340
341/// The main `#[pda(...)]` attribute macro. This is where the PDA seed ABI is defined.
342///
343/// usage example:
344/// ```ignore
345/// #[account]
346/// #[pda(
347/// seeds = [
348/// prefix = b"message",
349/// from: Pubkey,
350/// to: Pubkey,
351/// id: u64,
352/// ],
353/// // optional, defaults to the current crate ID
354/// program_id = crate::ID,
355/// // optional, defaults to all disabled
356/// features = [
357/// find, // generates "find_pda" method for struct
358/// assert // generates "assert_pda" method for struct
359/// ]
360/// )]
361/// pub struct Message {
362/// pub id: u64,
363/// }
364/// ```
365#[proc_macro_attribute]
366pub fn pda(args: TokenStream, input: TokenStream) -> TokenStream {
367 let args = parse_macro_input!(args as PdaArgs);
368 let item = parse_macro_input!(input as ItemStruct);
369
370 match args.expand(&item) {
371 Ok(tokens) => tokens.into(),
372 Err(err) => err.to_compile_error().into(),
373 }
374}
375
376/// The `#[pda_accounts]` attribute macro. This must be placed on any `struct` that contains fields using `pda(...)` bindings.
377///
378/// usage:
379/// ```ignore
380/// #[pda_accounts]
381/// #[derive(Accounts)]
382/// pub struct MyCtx<'info> {
383/// // example account using `pda(...)` binding with the `Message` struct defined in the previous example:
384/// #[account(pda(Message, [
385/// prefix,
386/// from = some_pubkey,
387/// to = some_other_pubkey,
388/// id = some_id,
389/// // ...
390/// ]))]
391/// pub my_pda_account: Account<'info, Message>,
392/// // ...
393/// }
394/// ```
395#[proc_macro_attribute]
396pub fn pda_accounts(args: TokenStream, input: TokenStream) -> TokenStream {
397 if !args.is_empty() {
398 return syn::Error::new(
399 proc_macro2::Span::call_site(),
400 "`#[pda_accounts]` does not take arguments",
401 )
402 .to_compile_error()
403 .into();
404 }
405
406 let item = parse_macro_input!(input as ItemStruct);
407
408 match accounts::expand::expand_pda_accounts(item) {
409 Ok(tokens) => tokens.into(),
410 Err(err) => err.to_compile_error().into(),
411 }
412}
413
414/// The `pda_signer!(...)` macro for generating CPI signer seeds.
415///
416/// usage example:
417/// ```ignore
418/// pda_signer!(
419/// let message_signer = Message,
420/// bump = ctx.bumps.message,
421/// [
422/// prefix,
423/// from = ctx.accounts.payer.key(),
424/// to = params.to,
425/// id = params.id,
426/// ]
427/// );
428/// ```
429///
430/// Then use `message_signer` in a `with_signer` call:
431/// ```ignore
432/// some_cpi_context.with_signer(message_signer).some_cpi_method(...);
433/// ```
434#[proc_macro]
435pub fn pda_signer(input: TokenStream) -> TokenStream {
436 let input = parse_macro_input!(input as signer::SignerInput);
437
438 match input.expand() {
439 Ok(tokens) => tokens.into(),
440 Err(err) => err.to_compile_error().into(),
441 }
442}