Expand description
§pda-buddy
pda-buddy is a small Anchor helper for keeping PDA seed definitions in one
place.
Anchor already has a good PDA model. This crate does not replace it. Instead, it lets you define the seed ABI once on the PDA account type, then reuse that definition in:
#[account(...)]constraints- CPI signer seed arrays
- optional helper methods for finding or asserting PDAs
The generated account constraints are normal Anchor-compatible
seeds = [...] and bump constraints, so IDL account resolution still works
the way Anchor expects.
§Runtime Cost
pda-buddy is a proc-macro crate. It does not add an on-chain runtime
dependency or a second PDA validation layer to your program.
Account bindings expand to Anchor’s native seeds = [...] and bump
constraints. Signer bindings expand to local stack values and a normal
&[&[&[u8]]] signer seed slice. In a normal optimized deployment, this
should have the same CU and program-size profile as writing the equivalent
Anchor code by hand.
The extra strict seed checks are Rust type checks generated at compile time.
They are emitted as no-op Anchor constraints that evaluate to true, so
optimized builds should remove them. Unchecked bindings with ! skip those
strict checks entirely.
§Quick Example
Define the PDA seed ABI on the account type:
use anchor_lang::prelude::*;
use pda_buddy::pda;
#[account]
#[pda(seeds = [
prefix = b"message",
from: Pubkey,
to: Pubkey,
id: u64,
])]
pub struct Message {
pub id: u64,
}Use that ABI in an Anchor accounts context:
use pda_buddy::pda_accounts;
#[pda_accounts]
#[derive(Accounts)]
#[instruction(params: Params)]
pub struct Initialize<'info> {
#[account(
init,
payer = payer,
space = 8 + core::mem::size_of::<Message>(),
pda(Message, [
prefix,
from = payer.key(),
to = params.to,
id = params.id,
])
)]
pub message: Account<'info, Message>,
#[account(mut)]
pub payer: Signer<'info>,
pub system_program: Program<'info, System>,
}pda-buddy expands the pda(...) binding into normal Anchor constraints:
seeds = [
b"message",
payer.key().as_ref(),
params.to.as_ref(),
params.id.to_le_bytes().as_ref(),
],
bumpBindings may be written in any order. The final generated seed order always
follows the original #[pda(seeds = [...])] definition.
§Account Constraints
Put #[pda_accounts] above #[derive(Accounts)]:
#[pda_accounts]
#[derive(Accounts)]
pub struct MyCtx<'info> {
// fields
}Inside an Anchor #[account(...)] attribute, add a top-level
pda(Type, [...]) item:
#[account(
mut,
pda(Message, [
prefix,
from = user.key(),
to = params.to,
id = params.id,
])
)]
pub message: Account<'info, Message>,Static seed bindings are written as just the label:
prefixDynamic seed bindings are written as label = expression:
from = user.key()
id = params.idFor Pubkey seeds, pass a Pubkey expression. If the seed value comes from
an Anchor account, call .key() explicitly:
from = payer.key()
to = params.toIf you intentionally want to bypass account or signer seed type checks for
one binding, append ! to the value. The generated seed expression is still
emitted normally, so the value must still support the generated seed
conversion such as .as_ref():
user = "custom-bytes"!§CPI Signer Seeds
Use pda_signer! when a PDA signs a CPI:
use pda_buddy::pda_signer;
pda_signer!(
let message_signer = Message,
bump = ctx.bumps.message,
[
prefix,
from = ctx.accounts.payer.key(),
to = params.to,
id = params.id,
]
);
some_cpi_context.with_signer(message_signer);The macro creates the temporary byte arrays needed for numeric seeds and bump bytes in the caller scope. This keeps signer seeds lifetime-safe without heap allocation.
Signer seed bindings also support the unchecked marker:
from = "wrong-but-i-want-this"!§Defining Seeds
Seeds are declared in #[pda(seeds = [...])].
#[pda(seeds = [
prefix = b"message",
authority: Pubkey,
slug: String,
index: u64,
shard: u16(be),
active: bool,
digest: [u8; 32],
])]
pub struct Message {
// account fields
}Static seeds use =.
prefix = b"message"Static seeds must be byte-string literals or resolvable byte-string consts:
impl Message {
pub const PREFIX: &'static [u8] = b"message";
}
#[pda(seeds = [
prefix = Self::PREFIX,
id: u64,
])]
pub struct Message {
pub id: u64,
}Dynamic seeds use :.
authority: Pubkey
id: u64Supported dynamic seed types:
| Seed type | Encoding |
|---|---|
Pubkey | pubkey.as_ref() |
bytes, Bytes, [u8], &[u8], Vec<u8> | as_ref() |
str, String | as_bytes() |
bool | one byte, 0 or 1 |
u8, i8 | one byte |
u16, u32, u64, i16, i32, i64 | little-endian by default |
u16(be), u32(be), u64(be), i16(be), i32(be), i64(be) | big-endian |
[u8; N] | fixed byte array |
u64 and u64(le) are equivalent. be is explicit big-endian.
§Program ID
By default, generated helper methods use crate::ID as the program id.
If the PDA belongs to another program, set program_id:
#[pda(
seeds = [
prefix = b"vault",
owner: Pubkey,
],
program_id = other_program::ID,
)]
pub struct Vault {
pub owner: Pubkey,
}The account-constraint rewrite still emits Anchor seeds and bump
constraints. Use Anchor’s native seeds::program = ... support separately
if you need cross-program PDA validation in an accounts context.
§Helper Methods
#[pda(...)] can generate helper methods:
#[pda(
seeds = [
prefix = b"message",
id: u64,
],
features = [find, assert],
)]
pub struct Message {
pub id: u64,
}find generates:
let (address, bump) = Message::find_pda(id, None);assert generates:
let bump = Message::assert_pda(&address, id, None)?;The final argument is an optional program id override:
let (address, bump) = Message::find_pda(id, Some(&custom_program_id));§Diagnostics
The macros validate seed labels and binding kinds. If a label is missing, duplicated, unknown, or bound in the wrong form, the compiler error includes the original PDA seed definition and the expected binding shape.
For example, if from: Pubkey is accidentally written as just from, the
error points out that the expected binding is:
from = <Pubkey expression>§Notes
#[pda_accounts]must appear above#[derive(Accounts)].pda(...)inside#[account(...)]must be a top-level account constraint item.- Static seed labels are bound by label only. Dynamic seed labels need
label = value. - 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. - This crate scans the consumer crate’s
src/**/*.rsfiles to find#[pda(...)]definitions. Keep PDA account definitions in normal source files undersrc.
Macros§
- pda_
signer - The
pda_signer!(...)macro for generating CPI signer seeds.
Attribute Macros§
- pda
- The main
#[pda(...)]attribute macro. This is where the PDA seed ABI is defined. - pda_
accounts - The
#[pda_accounts]attribute macro. This must be placed on anystructthat contains fields usingpda(...)bindings.