Skip to content

feat: resolve PDA defaults in instruction account builders#93

Draft
amilz wants to merge 2 commits intocodama-idl:mainfrom
amilz:feat/pda-defaults-in-builders
Draft

feat: resolve PDA defaults in instruction account builders#93
amilz wants to merge 2 commits intocodama-idl:mainfrom
amilz:feat/pda-defaults-in-builders

Conversation

@amilz
Copy link
Copy Markdown

@amilz amilz commented Mar 26, 2026

Summary

  • Instruction builders now auto-derive PDA-defaulted accounts instead of requiring .expect() / manual setting
  • Linked PDAs (with a corresponding account struct) call AccountName::find_pda(...)
  • Linked PDAs without an account struct, or cross-program PDAs, fall back to inline find_program_address
  • Accounts are topologically sorted by dependency so seeds resolve in the correct order
  • Extracted PDA resolution into a dedicated resolveInstructionPdaDefaults() helper

Bug fixes included

  • Silent incomplete seeds: return inside seeds.forEach only skipped one seed but registered the PDA with incomplete seeds → wrong address at runtime. Now bails the entire PDA resolution with a warning.
  • Circular dependency cascade: Only removed one node from cycle, leaving the other with a dangling ref → compile error. Now propagates fallback to all nodes in the cycle.
  • Wrong struct name for find_pda: Used the PDA node name instead of the account struct name. Broke when they differed (e.g. extensions PDA vs escrowExtensionsHeader account) or when no account existed for the PDA.
  • Spurious use solana_address; bare crate import removed.
  • Missing warnings: Added logWarn for unresolvable PDA nodes and missing seed values.

Generated output examples

creates_escrow builderescrow and event_authority auto-derive via linked find_pda:

pub fn instruction(&self) -> solana_instruction::Instruction {
    let payer = self.payer.expect("payer is not set");
    let admin = self.admin.expect("admin is not set");
    let escrow_seed = self.escrow_seed.expect("escrow_seed is not set");
    let escrow = self.escrow.unwrap_or_else(|| Escrow::find_pda(&escrow_seed).0);
    let system_program = self.system_program.unwrap_or(solana_address::address!("11111111111111111111111111111111"));
    let event_authority = self.event_authority.unwrap_or_else(|| EventAuthority::find_pda().0);
    // ...
}

deposit builder — mixed resolution: linked find_pda for accounts with structs, inline find_program_address for cross-program ATAs:

let allowed_mint = self.allowed_mint.unwrap_or_else(|| AllowedMint::find_pda(&escrow, &mint).0);
let receipt = self.receipt.unwrap_or_else(|| Receipt::find_pda(&escrow, &depositor, &mint, &receipt_seed).0);

// Cross-program ATA — no account struct, falls back to inline derivation
let vault = self.vault.unwrap_or_else(|| {
    solana_address::Address::find_program_address(
        &[escrow.as_ref(), token_program.as_ref(), mint.as_ref()],
        &crate::ESCROW_PROGRAM_ID,
    )
    .0
});

Docblock — PDA-defaulted accounts marked optional:

///   3. `[writable, optional]` escrow (default to PDA)
///   5. `[optional]` event_authority (default to PDA)

Test plan

  • 76/76 unit tests pass (14 new tests for PDA resolution)
  • All 4 e2e projects compile (dummy, system, memo, anchor)
  • Typecheck clean, lint + prettier clean
  • Tested locally against solana-program/escrow — verified cross-program PDA defaults (ATAs), linked PDAs, and dependency ordering

amilz added 2 commits March 25, 2026 16:17
Extract PDA resolution into a dedicated helper function with proper
error handling: warn on unresolvable PDA nodes, bail entirely on
missing seed values (instead of silently generating incomplete seeds),
and propagate circular dependency fallbacks to all affected nodes.

Also adds rawName to account-ref seeds so the template avoids fragile
string surgery, removes a spurious `use solana_address;` import, and
adds tests for programIdValueNode seeds, bytesTypeNode seeds, linked
PDAs with variable account seeds, circular dependencies, and
optional+PDA default interaction.
When a pdaLinkNode resolves to a PDA, use the corresponding account
struct's name for the find_pda() call — since find_pda is generated on
account structs, not PDA nodes. Falls back to inline
find_program_address when no account references the PDA.

Fixes broken codegen when PDA name differs from account name (e.g.
"extensions" PDA vs "escrowExtensionsHeader" account) and when a PDA
has no corresponding account struct at all.
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Mar 26, 2026

⚠️ No Changeset found

Latest commit: 995e2cc

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@amilz amilz self-assigned this Mar 26, 2026
@ioxde
Copy link
Copy Markdown

ioxde commented Mar 27, 2026

I've built something similar on top of v2.0.1 (Solana v3 dependencies aren't currently compatible with my repo) and having #23 as the base. I've integrated your code patterns and bug fixes as well. ioxde/main- 718eedf

I haven't had time to work out a proper PR (among other changes) because of the different base / current needs- but wanted to share this if it would help in anyway.

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.

2 participants