Skip to content

[2.x] Add relations, nested argument seeds and legacy PDAs to nodes-from-anchor - #1181

Merged
lorisleiva merged 1 commit into
mainfrom
09-25-add_relations_nested_argument_seeds_and_legacy_pdas_to_nodes-from-anchor
Sep 30, 2026
Merged

lorisleiva merged 1 commit into
mainfrom
09-25-add_relations_nested_argument_seeds_and_legacy_pdas_to_nodes-from-anchor

Conversation

@lorisleiva

@lorisleiva lorisleiva commented Sep 25, 2026 •

Copy link
Copy Markdown
Member

This PR extends @codama/nodes-from-anchor with Anchor IDL features that could not be represented before: nested argument PDA seeds, account relations, PDAs of legacy instruction accounts and type aliases.

Nested argument seeds

PDA seeds pointing to a nested field of an instruction argument (e.g. params.seed) now produce a dataValueNode to that path. The seed type is found by walking the argument through structs, following the program's defined types, and loses its Borsh size prefix like other seeds.

// { "kind": "arg", "path": "params.seed" }
pdaSeedValueNode('params_seed', dataValueNode('params.seed'));

A path to a field that does not exist throws ARGUMENT_TYPE_MISSING, whereas a path going through a type that is not a struct skips the PDA default value. Seeds pointing to a field of another account (e.g. mint.authority) also skip the PDA default value, since resolving them would require fetching that account.

Relations

Account relations (produced by has_one constraints) are recorded as an anchor.relations plugin on the instruction account carrying them, listing the identifiers of the related accounts (prefixed within nested account groups):

// { "name": "authority", "relations": ["vault"] }
instructionAccountNode({ identifier: 'authority', plugins: [pluginNode('anchor.relations', ['vault'])] });

Legacy IDLs list relations on the account declaring the constraint instead, so they are flipped onto their target accounts to produce the same plugin. Since legacy IDLs camelCase account names but keep the Rust casing in relations, they are matched by camelCase form.

Legacy instruction account PDAs

Legacy instruction accounts with a pda now get a PDA default value, like current IDLs. Legacy seeds carry their own types, and constant seeds and program IDs are supported, including program IDs given as 32 bytes. Legacy IDLs camelCase argument and account names but keep the Rust casing in seed paths (e.g. seed_a for the seedA argument), so seed paths are matched by camelCase form and resolve to the actual identifiers. PDAs whose seeds cannot be resolved get no default value.

Other changes

  • { kind: 'type', alias } type definitions are now unwrapped instead of throwing.
  • pdaSeedNodeFromAnchorV01, instructionAccountNodeFromAnchorV0x and instructionAccountNodesFromAnchorV0x take an options object ({ definedTypes, prefix }) instead of a positional prefix, and pdaSeedNodeFromAnchorV01 returns undefined for seeds that cannot be expressed statically.

Tests

New tests cover nested argument seeds (directly and through defined types), skipped nested account seeds, relations at the top level and within nested groups, flipped legacy relations, legacy PDA seeds and program IDs, and type aliases.

@changeset-bot

changeset-bot Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: e9f57f4

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

@lorisleiva
lorisleiva added this pull request to stack #1182 September 25, 2026 16:02
@lorisleiva

Copy link
Copy Markdown
Member Author

@trevor-cortex

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Summary

Extends @codama/nodes-from-anchor in four directions: (1) v01 arg seeds with nested paths (params.seed) now resolve their type by walking dataFields → definedTypeLinkNode → struct via a new getFieldPathType helper and produce dataValueNode('params.seed') (valid under the v2 PathString grammar); (2) relations are surfaced as an anchor.relations plugin on the target account (v01 as-is, v00 flipped since legacy IDLs list them on the has_one declarer); (3) legacy v00 instruction accounts with a pda now get a pdaValueNode default, including const/account/arg seeds and const/account/arg program IDs; (4) { kind: 'type', alias } type defs are unwrapped. pdaSeedNodeFromAnchorV01 now returns undefined for seeds it can't express statically, and the account converters take an options bag instead of a positional prefix.

I checked the flip direction against Anchor's 0.30 IDL generation: relations on an account list the sibling accounts whose has_one targets it, so the plugin semantics documented in anchorRelationsPluginNode match current IDLs and the v00 flip is the right transformation.

Things to watch out for

1. New hard-failure path for legacy IDLs (inline). Before this PR, pda on v00 instruction accounts was ignored entirely, so a legacy IDL could never fail on seeds. Now pdaSeedNodeFromAnchorV00 throws ARGUMENT_TYPE_MISSING when an arg seed's root doesn't match a data field. Legacy pda output was behind Anchor's seeds feature and varied across 0.2x releases — notably, legacy IDLs camelCase args[].name and accounts[].name while seed paths may carry the raw snake_case Rust field names. If that mismatch is real, the whole IDL conversion will now throw where it previously succeeded. I'd suggest returning undefined (skip the PDA default) in v00 rather than throwing, and verifying against a real legacy IDL that has pda seeds before merging. The same casing question applies to account seeds resolving to accountValueNode(seed.path).

2. No changeset. CONTRIBUTING asks for one on any user-facing change; this adds features and changes exported signatures (pdaSeedNodeFromAnchorV01, instructionAccountNode(s)FromAnchorV0x). The branch is in rc pre mode so the signature change is fine era-wise — but a minor changeset for @codama/nodes-from-anchor still seems warranted unless it's intentionally deferred to another PR in the stack.

3. Silent behavior change on v01 program seeds (good, but worth knowing). Previously a program seed that couldn't be expressed (e.g. path: 'mint.owner') fell through and produced a PDA implicitly bound to the current program — wrong. Now the default value is dropped. Correct fix, but downstream consumers relying on the old (incorrect) output will see PDA defaults disappear.

4. Asymmetry in v01 arg handling. A missing top-level arg throws, a missing nested field (params.nope) silently returns undefined. The description frames this as "must go through structs", but a nested field that doesn't exist is the same class of IDL inconsistency as a missing top-level arg. Not blocking — just flagging that the two paths behave differently.

Notes for subsequent reviewers

  • definedTypeMap in ProgramNode.ts is built from types (non-generic only, thanks to extractGenerics), so generic type defs can't hit GENERIC_TYPE_MISSING. Generic instantiations in args are inlined by structFieldTypeNodeFromAnchorV01, so getFieldPathType walks them directly. Account/event types are included in the map even though they're filtered from definedTypes — that's correct for lookup.
  • getFieldPathType doesn't resolve a definedTypeLinkNode at the last segment, so removeBorshSizePrefix won't strip the prefix through a string alias. Same limitation as top-level args today; fine to leave.
  • extractPdasVisitor already handles identifier collisions via fingerprinting, so the new v00 PDA defaults won't clash at the program level.
  • Tests cover the happy paths well. Missing: a v00 test for an arg seed whose root is absent from dataFields (whichever behavior is chosen for point 1), and a v01 test for program pointing at a nested account path returning no default.

Comment thread packages/nodes-from-anchor/src/v00/InstructionAccountNode.ts Outdated
Comment thread packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts Outdated
Comment thread packages/nodes-from-anchor/src/v00/InstructionAccountNode.ts Outdated
@lorisleiva
lorisleiva force-pushed the 09-25-add_relations_nested_argument_seeds_and_legacy_pdas_to_nodes-from-anchor branch from a9ab1b4 to d979a5c Compare September 27, 2026 09:53

@trevor-cortex trevor-cortex left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Re-review

All the points from my first pass have been addressed, and the new shape is cleaner than what I suggested:

  • v00 hard-failure path → gone. pdaSeedNodeFromAnchorV00 now returns undefined for anything it can't resolve, so a legacy IDL can never fail on PDA seeds (same as before the PR). The camelCase mismatch I speculated about turned out to be real and is handled generically via resolveFieldPath's matches callback + haveSameCamelCase, applied to arg seeds, account seeds (through resolveAccount/accountNames) and flipped relations. The resolved path uses the actual camelCased identifiers (vaultParams.vaultName), which is what downstream consumers need. Tests cover the unknown-arg, unknown-account and non-struct-nested cases.
  • v01 asymmetry → resolved. FieldPathResolution splits missing (throws, top-level or nested alike) from unreachable (skip), which is the right distinction and is now documented on pdaSeedNodeFromAnchorV01.
  • 32-byte const program IDs are decoded; value.value.value is gone; isDefined type guard replaces the ! assertions; the v01 nested-program test was added.

A few small things I double-checked and am fine with:

  • resolveDefinedTypeLinks guards against cross-program links and cycles, so a self-referential defined type can't loop.
  • accountNames is intentionally not forwarded into nested groups (the recursive call only spreads definedTypes/prefix), so each group resolves account seeds against its own siblings. Matches how legacy seeds paths are scoped.
  • v00 definedTypeMap includes account types as well as types; fine for lookup, and harmless duplication if a name appears in both.
  • typeNodeFromAnchorV01 is now run twice per defined type in programNodeFromAnchorV01 (once for definedTypes, once for the map). Negligible cost; not worth restructuring.

Still no changeset. Since this targets a stack branch (09-25-adapt_nodes-from-anchor_to_codama_v2) I assume the release note is handled at the base of the stack — just flagging so it isn't forgotten before the stack lands on main.

One lint-level nit inline on import ordering in utils.ts.

Comment thread packages/nodes-from-anchor/src/utils.ts Outdated
@lorisleiva
lorisleiva force-pushed the 09-25-add_relations_nested_argument_seeds_and_legacy_pdas_to_nodes-from-anchor branch 2 times, most recently from 94fe34a to aca2adc Compare September 30, 2026 14:07
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_nodes-from-anchor_to_codama_v2 branch 2 times, most recently from 822c4db to 35fe188 Compare September 30, 2026 14:08
@lorisleiva
lorisleiva force-pushed the 09-25-add_relations_nested_argument_seeds_and_legacy_pdas_to_nodes-from-anchor branch from aca2adc to 2bb3538 Compare September 30, 2026 14:08
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_nodes-from-anchor_to_codama_v2 branch from 35fe188 to 7ada96f Compare September 30, 2026 14:09
@lorisleiva
lorisleiva force-pushed the 09-25-add_relations_nested_argument_seeds_and_legacy_pdas_to_nodes-from-anchor branch 2 times, most recently from 0554451 to df64b9f Compare September 30, 2026 14:11
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_nodes-from-anchor_to_codama_v2 branch 2 times, most recently from b5a4252 to d1a13ad Compare September 30, 2026 14:12
@lorisleiva
lorisleiva force-pushed the 09-25-add_relations_nested_argument_seeds_and_legacy_pdas_to_nodes-from-anchor branch 2 times, most recently from 9c2e80a to 3dd3fd7 Compare September 30, 2026 14:13
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_nodes-from-anchor_to_codama_v2 branch from d1a13ad to 031ad68 Compare September 30, 2026 14:13
@lorisleiva
lorisleiva force-pushed the 09-25-add_relations_nested_argument_seeds_and_legacy_pdas_to_nodes-from-anchor branch from 9c2e80a to 3dd3fd7 Compare September 30, 2026 14:13
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_nodes-from-anchor_to_codama_v2 branch 2 times, most recently from 031ad68 to e51263e Compare September 30, 2026 14:14
@lorisleiva
lorisleiva force-pushed the 09-25-add_relations_nested_argument_seeds_and_legacy_pdas_to_nodes-from-anchor branch from 3dd3fd7 to 0d35b27 Compare September 30, 2026 14:14
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_nodes-from-anchor_to_codama_v2 branch from e51263e to d435238 Compare September 30, 2026 14:15
@lorisleiva
lorisleiva force-pushed the 09-25-add_relations_nested_argument_seeds_and_legacy_pdas_to_nodes-from-anchor branch 2 times, most recently from 497ad44 to 8955bbe Compare September 30, 2026 14:16
@lorisleiva
lorisleiva force-pushed the 09-25-adapt_nodes-from-anchor_to_codama_v2 branch from d435238 to 563892d Compare September 30, 2026 14:16
Base automatically changed from 09-25-adapt_nodes-from-anchor_to_codama_v2 to main September 30, 2026 14:17
@lorisleiva
lorisleiva force-pushed the 09-25-add_relations_nested_argument_seeds_and_legacy_pdas_to_nodes-from-anchor branch from 8955bbe to e9f57f4 Compare September 30, 2026 14:17
@lorisleiva
lorisleiva marked this pull request as ready for review September 30, 2026 14:17
@lorisleiva
lorisleiva merged commit 20bbdc7 into main Sep 30, 2026
2 of 4 checks passed
@lorisleiva
lorisleiva deleted the 09-25-add_relations_nested_argument_seeds_and_legacy_pdas_to_nodes-from-anchor branch September 30, 2026 14:17
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