Resolve nested seed paths in v01 PDA extraction#990
Resolve nested seed paths in v01 PDA extraction#990ioxde wants to merge 7 commits intocodama-idl:mainfrom
Conversation
🦋 Changeset detectedLatest commit: 48b8986 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
4902b1e to
60bfce9
Compare
trevor-cortex
left a comment
There was a problem hiding this comment.
Summary
This PR resolves nested seed paths (e.g. mint.authority, args.owner) in v01 PDA extraction, which were previously left as a TODO and silently skipped. It introduces a resolveNestedFieldType helper that walks struct fields (inline or via IDL type definitions) to determine the leaf type. The function pdaSeedNodeFromAnchorV01 can now return undefined when resolution fails, and callers handle this gracefully by skipping the PDA default value.
Additionally, the PR adds self-referential seed detection — if a seed references the account being derived, the PDA default is skipped with a warning.
Key things to watch
-
Breaking return type change:
pdaSeedNodeFromAnchorV01now returns| undefined. This is a well-handled breaking change internally — all call sites check forundefined— but any external consumers of this export would need updating. -
Self-referential detection: The new self-referential check (lines 131–138 in
InstructionAccountNode.ts) correctly catches cases likevaultreferencing itself, and nested paths likeguard.mintwhere the account name matches the seed's account reference. The test formy_guardwithpath: 'my_guard.mint'is a good edge case. -
Nested program paths: Program seeds with nested paths are still explicitly skipped with a warning — this is a reasonable incremental choice since program seeds with nested paths are rare.
Notes for subsequent reviewers
- The
resolveNestedFieldTypehelper only handlesstructTypeNodeanddefinedTypeLinkNode. Other wrappers (e.g.optionTypeNode) would fall through and returnundefined. This seems fine for now — real-world IDL seeds shouldn't reference optional fields — but worth keeping in mind. - The
InstructionNode.test.tschange for thedistributionaccount is interesting: it confirms the self-referential detection works at the integration level (the accountdistributionhas a seedpath: 'distribution.group_mint'referencing itself, so the PDA default is correctly omitted). - Test coverage is thorough: nested account paths, nested arg paths, unresolvable types, self-referential accounts, prefixed nested groups, and program seed edge cases are all covered.
| // [ | ||
| // accountNode({ | ||
| // data: sizePrefixTypeNode( | ||
| // structTypeNode([structFieldTypeNode({ name: 'authority', type: publicKeyTypeNode() })]), | ||
| // numberTypeNode('u32'), | ||
| // ), | ||
| // name: 'mint', | ||
| // }), | ||
| // ], |
There was a problem hiding this comment.
The idea was that these would be uncommented when nested arguments/accounts are supported. Is the idea that the new tests are taking care of that deleted code now?
There was a problem hiding this comment.
The commented code assumed account data would be passed directly via accountNode, but nested types are now resolved through idlTypes instead.
| // [ | ||
| // accountNode({ | ||
| // data: sizePrefixTypeNode( | ||
| // structTypeNode([structFieldTypeNode({ name: 'authority', type: publicKeyTypeNode() })]), | ||
| // numberTypeNode('u32'), | ||
| // ), | ||
| // name: 'mint', | ||
| // }), | ||
| // ], |
| if (!resolved) { | ||
| logWarn(`Could not resolve nested account path "${seed.path}" for PDA seed.`); | ||
| return undefined; | ||
| } |
There was a problem hiding this comment.
I wonder if here we should just throw? Before we were silently ignoring nested paths because that was a valid scenario we were not able to support. Now that we are, if the nested seed don't resolve properly than it is a malformation on the IDL itself right?
There was a problem hiding this comment.
Honestly not sure. I know Anchor PDAs aren't perfect. I think the best path would be to throw by default (with a detailed message pointing to docs/what to fix) but allowing a user to override per error. I am not sure that whole change fits in this PR though. Seems more like a project-level standard / convention?
I personally do like the fail-first override behavior because it forces the user to understand their decisions. Console warnings don't always tell the real severity of the issue / easy to accidentally ignore.
I'll wait for more direction to make changes here.
| } | ||
|
|
||
| function resolveNestedFieldType( | ||
| rootType: TypeNode, |
There was a problem hiding this comment.
I wouldn't call that rootType since there is a RootNode which makes this confusing.
| // TODO: Handle seeds with nested paths. (Needs a path in the IDL but should we?) | ||
| // defaultValue: pdaValueNode( | ||
| // pdaNode({ | ||
| // name: 'distribution', | ||
| // seeds: [ | ||
| // constantPdaSeedNodeFromBytes('base58', 'F9bS'), | ||
| // variablePdaSeedNode('distributionGroupMint', publicKeyTypeNode()), | ||
| // ], | ||
| // }), | ||
| // [pdaSeedValueNode("distributionGroupMint", accountValueNode('distribution', 'group_mint'))], | ||
| // ), |
There was a problem hiding this comment.
Same feedback about using idlTypes as above.
| expect(nodes.definition).toEqual(constantPdaSeedNodeFromBytes('base58', 'HeLLo')); | ||
| expect(nodes.value).toBeUndefined(); | ||
| expect(nodes!.definition).toEqual(constantPdaSeedNodeFromBytes('base58', 'HeLLo')); | ||
| expect(nodes!.value).toBeUndefined(); |
There was a problem hiding this comment.
If we throw instead of returning undefined, we remove the need for these casts.
There was a problem hiding this comment.
I switched all tests to ?.. Unsure about the throw. See above comment about throw vs log.
* found while rebasing onto main
|
I think I addressed all comments outside of the |
This is a direct follow-up to #984 which left the nested path handling as a TODO.
PDA seeds with nested paths like
mint.authorityorargs.ownerwere skipped. This PR resolves them by looking up the field type from IDL type definitions.{ kind: 'account', path: 'mint.authority' }+ aMinttype def withauthority: pubkey->variablePdaSeedNode('mintAuthority', publicKeyTypeNode()){ kind: 'arg', path: 'args.owner' }whereargsis a struct withowner: pubkey->variablePdaSeedNode('owner', publicKeyTypeNode())Program seeds with nested paths still skip (with a warning).
pdaSeedNodeFromAnchorV01can now returnundefined.