Repository navigation
[2.x] Add relations, nested argument seeds and legacy PDAs to nodes-from-anchor - #1181
Conversation
|
trevor-cortex
left a comment
There was a problem hiding this comment.
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
definedTypeMapinProgramNode.tsis built fromtypes(non-generic only, thanks toextractGenerics), so generic type defs can't hitGENERIC_TYPE_MISSING. Generic instantiations in args are inlined bystructFieldTypeNodeFromAnchorV01, sogetFieldPathTypewalks them directly. Account/event types are included in the map even though they're filtered fromdefinedTypes— that's correct for lookup.getFieldPathTypedoesn't resolve adefinedTypeLinkNodeat the last segment, soremoveBorshSizePrefixwon't strip the prefix through a string alias. Same limitation as top-level args today; fine to leave.extractPdasVisitoralready 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
argseed whose root is absent fromdataFields(whichever behavior is chosen for point 1), and a v01 test forprogrampointing at a nested account path returning no default.
a9ab1b4 to
d979a5c
Compare
trevor-cortex
left a comment
There was a problem hiding this comment.
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.
pdaSeedNodeFromAnchorV00now returnsundefinedfor 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 viaresolveFieldPath'smatchescallback +haveSameCamelCase, applied toargseeds,accountseeds (throughresolveAccount/accountNames) and flippedrelations. 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.
FieldPathResolutionsplitsmissing(throws, top-level or nested alike) fromunreachable(skip), which is the right distinction and is now documented onpdaSeedNodeFromAnchorV01. - 32-byte const program IDs are decoded;
value.value.valueis gone;isDefinedtype guard replaces the!assertions; the v01 nested-programtest was added.
A few small things I double-checked and am fine with:
resolveDefinedTypeLinksguards against cross-program links and cycles, so a self-referential defined type can't loop.accountNamesis intentionally not forwarded into nested groups (the recursive call only spreadsdefinedTypes/prefix), so each group resolves account seeds against its own siblings. Matches how legacyseedspaths are scoped.- v00
definedTypeMapincludes account types as well astypes; fine for lookup, and harmless duplication if a name appears in both. typeNodeFromAnchorV01is now run twice per defined type inprogramNodeFromAnchorV01(once fordefinedTypes, 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.
d979a5c to
16b2a55
Compare
94fe34a to
aca2adc
Compare
822c4db to
35fe188
Compare
aca2adc to
2bb3538
Compare
35fe188 to
7ada96f
Compare
0554451 to
df64b9f
Compare
b5a4252 to
d1a13ad
Compare
9c2e80a to
3dd3fd7
Compare
d1a13ad to
031ad68
Compare
9c2e80a to
3dd3fd7
Compare
031ad68 to
e51263e
Compare
3dd3fd7 to
0d35b27
Compare
e51263e to
d435238
Compare
497ad44 to
8955bbe
Compare
d435238 to
563892d
Compare
8955bbe to
e9f57f4
Compare

This PR extends
@codama/nodes-from-anchorwith 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 adataValueNodeto 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.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 byhas_oneconstraints) are recorded as ananchor.relationsplugin on the instruction account carrying them, listing the identifiers of the related accounts (prefixed within nested account groups):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
pdanow 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_afor theseedAargument), 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,instructionAccountNodeFromAnchorV0xandinstructionAccountNodesFromAnchorV0xtake an options object ({ definedTypes, prefix }) instead of a positional prefix, andpdaSeedNodeFromAnchorV01returnsundefinedfor 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.