Skip to content

Add CodamaPdaHelpers derive - #86

Open
grod220 wants to merge 1 commit into
codama-idl:mainfrom
grod220:pda-helpers
Open

grod220 wants to merge 1 commit into
codama-idl:mainfrom
grod220:pda-helpers

Conversation

@grod220

@grod220 grod220 commented Mar 25, 2026

Copy link
Copy Markdown
Contributor

Adds #[derive(CodamaPdaHelpers)] which generates runtime PDA helper methods from existing #[codama(seed(...))] attributes. This reduces repeated manual PDA boilerplate program devs are writing.

API

use codama::{CodamaPda, CodamaPdaHelpers};

#[derive(CodamaPda, CodamaPdaHelpers)]
#[codama(seed(type = string(utf8), value = "mint"))]
#[codama(seed(name = "unwrappedMint", type = public_key))]
#[codama(seed(name = "wrappedTokenProgram", type = public_key))]
pub struct WrappedMintPda;

// ==== In program ==== 

let (address, bump) = WrappedMintPda::derive_program_address(
    &unwrapped_mint,
    &wrapped_token_program,
    &program_id,
)?;

let signer_seeds: [Seed; 4] = WrappedMintPda::signer_seeds(
    &unwrapped_mint,
    &wrapped_token_program,
    &bump_bytes,
);
  • Implemented macro expansion that parses struct-level #[codama(seed(...))] directives and generates:
    • seeds
    • seeds_with_bump
    • signer_seeds
    • (wrapping Address methods below)
    • derive_address
    • create_program_address
    • find_program_address
    • try_find_program_address
    • derive_program_address
  • Supports constant seeds (string(utf8), integer number(...)) and variable/linked seeds
  • Handles seed name deduplication, identifier collision detection, reserved bump, and Rust keyword escaping

Discussion points

  • Better name?
  • Should all of those Address methods be shipped? Aka, should we drop find_program_address?
  • The intention was for program usage, but might clients also need this?
  • An impl vs external crate helpers
  • How does this sit next to the CPI helpers PR (link?)

Comment thread codama-macros/src/lib.rs
Comment on lines +62 to +63
#[proc_macro_derive(CodamaPdaHelpers, attributes(codama))]
pub fn codama_pda_helpers_derive(input: TokenStream) -> TokenStream {

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@febo better naming proposals?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whatever we land on with the derive name, just want to flag that IMO it should start with Codama since it purely relies on codama attributes.

Comment on lines +296 to +301
/// (wrapping `Address` methods)
/// - `derive_address`
/// - `create_program_address`
/// - `find_program_address`
/// - `try_find_program_address`
/// - `derive_program_address`

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@febo thoughts on which Address methods this should include? This sort of wraps everything, but maybe it should be a blessed subset.

@lorisleiva lorisleiva left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go through this thoroughly asap but I just wanted to share some early first impression feedback.

@@ -0,0 +1,97 @@
use heck::ToSnakeCase;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we already have an exported CamelCaseString struct in codama-nodes maybe it would be cleaner / more consistent to also add a SnakeCaseString struct there instead of introducing a new dependency.

Comment thread codama-macros/src/lib.rs
Comment on lines +62 to +75
#[proc_macro_derive(CodamaPdaHelpers, attributes(codama))]
pub fn codama_pda_helpers_derive(input: TokenStream) -> TokenStream {
#[cfg(not(target_os = "solana"))]
{
pda_helpers::codama_pda_helpers_derive_impl(input.into())
.unwrap_or_else(codama_errors::CodamaError::into_compile_error)
.into()
}
#[cfg(target_os = "solana")]
{
input
}
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It would be nice if this macro was on its own crate. Maybe something like codama-pda-helpers or whatever the name ends up being here.

That way, if anyone wants to use a slightly different version of our generated code, it's easier for them to fork that crate on its own.

@aditya-able

Copy link
Copy Markdown

@lorisleiva @grod220 I opened #NNN with the SnakeCaseString struct from the comment above, so this can drop the heck dependency.

Gabe, if you are still on this I will leave the ident.rs change to you. If you have moved on, happy to pick it up, rebase, and switch it over.

@grod220

grod220 commented Aug 24, 2026

Copy link
Copy Markdown
Contributor Author

@aditya-able, I've moved on to some other things, but if you find it useful, I encourage you to push things forward! Feel free to take over this branch/PR or another 👍

senzenn added a commit to senzenn/codama-rs that referenced this pull request Sep 2, 2026
Same shape as CamelCaseString: same derives, new<T: AsRef<str>>, the String
and &str conversions, Deref and AsRef, and the tests in the same order.

Word boundaries deliberately do not follow CamelCaseString. to_camel_case
reads a run of uppercase letters as one word, turning tokenAMint into
tokenAmint and SPLToken into spltoken. In snake_case that would give
token_amint and spltoken, which make poor identifiers, and tokenAMint and
tokenBMint show up often enough in real PDA seeds to matter. So an uppercase
run splits before its last letter when a lowercase letter follows.

Digits carry the case of whatever preceded them, so a digit ends a word only
when an uppercase letter follows a lowercase run. seed2Bump gives seed2_bump
while mint_2, this123 and 2Mint are left alone.

That combination is exactly heck::ToSnakeCase, checked by differential
fuzzing against heck over 1,000,021 inputs with zero mismatches, so codama-idl#86 can
drop the dependency without a change in behaviour.
senzenn added a commit to senzenn/codama-rs that referenced this pull request Sep 2, 2026
Same shape as CamelCaseString: same derives, new<T: AsRef<str>>, the String
and &str conversions, Deref and AsRef, and the tests in the same order.

Word boundaries deliberately do not follow CamelCaseString. to_camel_case
reads a run of uppercase letters as one word, turning tokenAMint into
tokenAmint and SPLToken into spltoken. In snake_case that would give
token_amint and spltoken, which make poor identifiers, and tokenAMint and
tokenBMint show up often enough in real PDA seeds to matter. So an uppercase
run splits before its last letter when a lowercase letter follows.

Digits carry the case of whatever preceded them, so a digit ends a word only
when an uppercase letter follows a lowercase run. seed2Bump gives seed2_bump
while mint_2, this123 and 2Mint are left alone.

That combination matches heck::ToSnakeCase, checked by differential fuzzing
over 1,000,021 ASCII inputs with zero mismatches. On a further 400,000
Unicode inputs the only divergence is a word-final Greek capital sigma, which
heck lowercases to the final form while this lowercases per character, as
to_camel_case does. So codama-idl#86 can drop the dependency.
senzenn added a commit to senzenn/codama-rs that referenced this pull request Sep 2, 2026
Same shape as CamelCaseString: same derives, new<T: AsRef<str>>, the String
and &str conversions, Deref and AsRef, and the tests in the same order.

Word boundaries deliberately do not follow CamelCaseString. to_camel_case
reads a run of uppercase letters as one word, turning tokenAMint into
tokenAmint and SPLToken into spltoken. In snake_case that would give
token_amint and spltoken, which make poor identifiers, and tokenAMint and
tokenBMint show up often enough in real PDA seeds to matter. So an uppercase
run splits before its last letter when a lowercase letter follows.

Digits carry the case of whatever preceded them, so a digit ends a word only
when an uppercase letter follows a lowercase run. seed2Bump gives seed2_bump
while mint_2, this123 and 2Mint are left alone.

That combination matches heck::ToSnakeCase, checked by differential fuzzing
over 1,000,021 ASCII inputs with zero mismatches. On a further 400,000
Unicode inputs the only divergence is a word-final Greek capital sigma, which
heck lowercases to the final form while this lowercases per character, as
to_camel_case does. So codama-idl#86 can drop the dependency.
@senzenn

senzenn commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

@lorisleiva happy to take this over if it's still wanted Gabe gave the go-ahead above. Before I rebase, two v2 questions, since the answers change what I'd build:

  1. Timing: codama-rs is going to 1.0.0 to freeze the v1 API before the v2 work lands. Should this go into 1.x, or wait and target v2 directly?

  2. Scope: is the seed attribute inventory settled yet specifically whether PDA seed pairing joins? If seed references become path expressions, the seed parsing here changes shape, and I want to know if that reaches seed(name = ...). [ref the issue where that's tracked]

On casing: agreed on #136, no new type in codama-nodes. If you want to keep heck out, I have to_snake_case as a plain function in codama-syn-helpers alongside the other syn helpers. It matches heck::ToSnakeCase on ASCII, fuzzed over a million inputs with zero mismatches. Equally fine leaving heck in it's one line and not the interesting part here.

If the answer is wait for v2, I'd rather spend the time on the identifier rename instead. Point me at whichever is more useful.

@lorisleiva

Copy link
Copy Markdown
Member

@senzenn Since v2 is just around the corner I'd rather not make any radical changes to v1 in the meantime since we otherwise risk to miss some features when forward-porting them to v2.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants