Skip to main content

Crate pda_buddy

Crate pda_buddy 

Source
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(),
],
bump

Bindings 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:

prefix

Dynamic seed bindings are written as label = expression:

from = user.key()
id = params.id

For Pubkey seeds, pass a Pubkey expression. If the seed value comes from an Anchor account, call .key() explicitly:

from = payer.key()
to = params.to

If 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: u64

Supported dynamic seed types:

Seed typeEncoding
Pubkeypubkey.as_ref()
bytes, Bytes, [u8], &[u8], Vec<u8>as_ref()
str, Stringas_bytes()
boolone byte, 0 or 1
u8, i8one byte
u16, u32, u64, i16, i32, i64little-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/**/*.rs files to find #[pda(...)] definitions. Keep PDA account definitions in normal source files under src.

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 any struct that contains fields using pda(...) bindings.