From ad3e2c1f461fdeaf6a4a291cd2da778ae60f05db Mon Sep 17 00:00:00 2001 From: ioxde <228087182+ioxde@users.noreply.github.com> Date: Tue, 14 Apr 2026 15:49:42 -0700 Subject: [PATCH 1/7] Local test fixes --- packages/dynamic-client/package.json | 2 +- packages/dynamic-client/test/programs/anchor/tests/blog.test.ts | 2 +- packages/dynamic-client/test/programs/idls/blog-idl.json | 2 +- packages/dynamic-client/test/programs/idls/example-idl.json | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/dynamic-client/package.json b/packages/dynamic-client/package.json index 8bd183c40..8bf598060 100644 --- a/packages/dynamic-client/package.json +++ b/packages/dynamic-client/package.json @@ -56,7 +56,7 @@ "lint:fix": "pnpm generate-program-types && eslint --fix . && prettier --write .", "test": "pnpm test:setup && pnpm test:types && pnpm test:treeshakability && pnpm test:unit", "test:setup": "pnpm test:anchor:build && ( [ -n \"$CI\" ] || pnpm generate-idl-from-anchor ) && pnpm generate-program-types", - "test:anchor:build": "cd test/programs/anchor && anchor build", + "test:anchor:build": "cd test/programs/anchor && anchor build --ignore-keys", "test:treeshakability": "for file in dist/index.*.mjs; do agadoo $file; done", "test:types": "tsc --noEmit", "test:unit": "vitest run", diff --git a/packages/dynamic-client/test/programs/anchor/tests/blog.test.ts b/packages/dynamic-client/test/programs/anchor/tests/blog.test.ts index df4c383a3..dea387a02 100644 --- a/packages/dynamic-client/test/programs/anchor/tests/blog.test.ts +++ b/packages/dynamic-client/test/programs/anchor/tests/blog.test.ts @@ -10,7 +10,7 @@ import { loadIdl, SvmTestContext } from '../../test-utils'; const idl = loadIdl('blog-idl.json'); const programClient = createProgramClient(idl); -const programSoPath = path.resolve(__dirname, '..', '..', 'dumps', 'blog.so'); +const programSoPath = path.resolve(__dirname, '..', 'target', 'deploy', 'blog.so'); describe('blog', () => { let ctx: SvmTestContext; diff --git a/packages/dynamic-client/test/programs/idls/blog-idl.json b/packages/dynamic-client/test/programs/idls/blog-idl.json index a08261557..9f91b658c 100644 --- a/packages/dynamic-client/test/programs/idls/blog-idl.json +++ b/packages/dynamic-client/test/programs/idls/blog-idl.json @@ -1,7 +1,7 @@ { "kind": "rootNode", "standard": "codama", - "version": "1.5.1", + "version": "1.6.0", "program": { "kind": "programNode", "name": "blog", diff --git a/packages/dynamic-client/test/programs/idls/example-idl.json b/packages/dynamic-client/test/programs/idls/example-idl.json index c18a34147..f4fc4147d 100644 --- a/packages/dynamic-client/test/programs/idls/example-idl.json +++ b/packages/dynamic-client/test/programs/idls/example-idl.json @@ -1,7 +1,7 @@ { "kind": "rootNode", "standard": "codama", - "version": "1.5.1", + "version": "1.6.0", "program": { "kind": "programNode", "name": "example", From 7488ec98efd365f0c843ca91adf8284f6ae018c5 Mon Sep 17 00:00:00 2001 From: ioxde <228087182+ioxde@users.noreply.github.com> Date: Tue, 7 Apr 2026 10:31:06 -0700 Subject: [PATCH 2/7] Resolve nested seed paths in v01 PDA extraction --- .changeset/nested-pda-seeds.md | 5 + .../src/v01/InstructionAccountNode.ts | 101 ++++-- .../src/v01/InstructionNode.ts | 16 +- .../nodes-from-anchor/src/v01/PdaSeedNode.ts | 125 +++++-- .../nodes-from-anchor/src/v01/ProgramNode.ts | 2 +- .../test/v01/InstructionAccountNode.test.ts | 316 ++++++++++++++++-- .../test/v01/InstructionNode.test.ts | 12 +- .../test/v01/pdaSeedNode.test.ts | 81 ++++- 8 files changed, 560 insertions(+), 98 deletions(-) create mode 100644 .changeset/nested-pda-seeds.md diff --git a/.changeset/nested-pda-seeds.md b/.changeset/nested-pda-seeds.md new file mode 100644 index 000000000..76e052680 --- /dev/null +++ b/.changeset/nested-pda-seeds.md @@ -0,0 +1,5 @@ +--- +'@codama/nodes-from-anchor': minor +--- + +Resolve nested seed paths in v01 PDA extraction instead of silently skipping them diff --git a/packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts b/packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts index 5de7a77d2..93c3f831f 100644 --- a/packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts +++ b/packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts @@ -1,3 +1,4 @@ +import { logWarn } from '@codama/errors'; import { AccountValueNode, ArgumentValueNode, @@ -7,16 +8,15 @@ import { InstructionArgumentNode, isNode, pdaNode, - PdaSeedNode, - PdaSeedValueNode, PdaValueNode, pdaValueNode, PublicKeyValueNode, publicKeyValueNode, } from '@codama/nodes'; -import { IdlV01InstructionAccount, IdlV01InstructionAccountItem, IdlV01Seed } from './idl'; +import { IdlV01InstructionAccount, IdlV01InstructionAccountItem, IdlV01TypeDef } from './idl'; import { pdaSeedNodeFromAnchorV01 } from './PdaSeedNode'; +import type { GenericsV01 } from './unwrapGenerics'; function hasDuplicateAccountNames(idl: IdlV01InstructionAccountItem[]): boolean { const seenNames = new Set(); @@ -45,6 +45,8 @@ export function instructionAccountNodesFromAnchorV01( idl: IdlV01InstructionAccountItem[], instructionArguments: InstructionArgumentNode[], prefix?: string, + idlTypes: IdlV01TypeDef[] = [], + generics: GenericsV01 = { constArgs: {}, typeArgs: {}, types: {} }, ): InstructionAccountNode[] { const shouldPrefix = prefix !== undefined || hasDuplicateAccountNames(idl); @@ -54,8 +56,18 @@ export function instructionAccountNodesFromAnchorV01( account.accounts, instructionArguments, shouldPrefix ? (prefix ? `${prefix}_${account.name}` : account.name) : undefined, + idlTypes, + generics, ) - : [instructionAccountNodeFromAnchorV01(account, instructionArguments, shouldPrefix ? prefix : undefined)], + : [ + instructionAccountNodeFromAnchorV01( + account, + instructionArguments, + shouldPrefix ? prefix : undefined, + idlTypes, + generics, + ), + ], ); } @@ -63,6 +75,8 @@ export function instructionAccountNodeFromAnchorV01( idl: IdlV01InstructionAccount, instructionArguments: InstructionArgumentNode[], prefix?: string, + idlTypes: IdlV01TypeDef[] = [], + generics: GenericsV01 = { constArgs: {}, typeArgs: {}, types: {} }, ): InstructionAccountNode { const isOptional = idl.optional ?? false; const docs = idl.docs ?? []; @@ -74,38 +88,61 @@ export function instructionAccountNodeFromAnchorV01( if (idl.address) { defaultValue = publicKeyValueNode(idl.address, name); } else if (idl.pda) { - // TODO: Handle seeds with nested paths. - // Currently, we gracefully ignore PDA default values if we encounter seeds with nested paths. - const seedsWithNestedPaths = idl.pda.seeds.some(seed => 'path' in seed && seed.path.includes('.')); - if (!seedsWithNestedPaths) { - const [seedDefinitions, seedValues] = idl.pda.seeds.reduce( - ([seeds, lookups], seed: IdlV01Seed) => { - const { definition, value } = pdaSeedNodeFromAnchorV01(seed, instructionArguments, prefix); - return [[...seeds, definition], value ? [...lookups, value] : lookups]; - }, - <[PdaSeedNode[], PdaSeedValueNode[]]>[[], []], + const hasNestedProgramPath = + idl.pda.program != null && 'path' in idl.pda.program && idl.pda.program.path.includes('.'); + if (hasNestedProgramPath) { + logWarn(`Skipping PDA for account "${name}": program seed uses a nested path that cannot be resolved.`); + } else { + const seedResults = idl.pda.seeds.map(seed => + pdaSeedNodeFromAnchorV01(seed, instructionArguments, prefix, idlTypes, generics), ); - let programId: string | undefined; - let programIdValue: AccountValueNode | ArgumentValueNode | undefined; - if (idl.pda.program !== undefined) { - const { definition, value } = pdaSeedNodeFromAnchorV01(idl.pda.program, instructionArguments, prefix); - if ( - isNode(definition, 'constantPdaSeedNode') && - isNode(definition.value, 'bytesValueNode') && - definition.value.encoding === 'base58' - ) { - programId = definition.value.data; - } else if (value && isNode(value.value, ['accountValueNode', 'argumentValueNode'])) { - programIdValue = value.value; + if (seedResults.every((r): r is NonNullable => r != null)) { + const seedDefinitions = seedResults.map(r => r.definition); + const seedValues = seedResults.flatMap(r => (r.value ? [r.value] : [])); + + let programId: string | undefined; + let programIdValue: AccountValueNode | ArgumentValueNode | undefined; + if (idl.pda.program !== undefined) { + const result = pdaSeedNodeFromAnchorV01( + idl.pda.program, + instructionArguments, + prefix, + idlTypes, + generics, + ); + if (!result) { + logWarn(`Skipping PDA for account "${name}": program seed could not be resolved.`); + return instructionAccountNode({ defaultValue, docs, isOptional, isSigner, isWritable, name }); + } + if ( + isNode(result.definition, 'constantPdaSeedNode') && + isNode(result.definition.value, 'bytesValueNode') && + result.definition.value.encoding === 'base58' + ) { + programId = result.definition.value.data; + } else if (result.value && isNode(result.value.value, ['accountValueNode', 'argumentValueNode'])) { + programIdValue = result.value.value; + } } - } - defaultValue = pdaValueNode( - pdaNode({ name, programId, seeds: seedDefinitions }), - seedValues, - programIdValue, - ); + const camelName = camelCase(name); + const isSelfReferential = + seedValues.some(sv => isNode(sv.value, 'accountValueNode') && sv.value.name === camelName) || + (programIdValue != null && + isNode(programIdValue, 'accountValueNode') && + programIdValue.name === camelName); + if (isSelfReferential) { + logWarn(`Skipping PDA for account "${name}": a seed references the account itself.`); + } + if (!isSelfReferential) { + defaultValue = pdaValueNode( + pdaNode({ name, programId, seeds: seedDefinitions }), + seedValues, + programIdValue, + ); + } + } } } diff --git a/packages/nodes-from-anchor/src/v01/InstructionNode.ts b/packages/nodes-from-anchor/src/v01/InstructionNode.ts index ca7cdd5c1..e93ce1db0 100644 --- a/packages/nodes-from-anchor/src/v01/InstructionNode.ts +++ b/packages/nodes-from-anchor/src/v01/InstructionNode.ts @@ -9,12 +9,16 @@ import { } from '@codama/nodes'; import { getAnchorDiscriminatorV01 } from '../discriminators'; -import type { IdlV01Instruction } from './idl'; +import type { IdlV01Instruction, IdlV01TypeDef } from './idl'; import { instructionAccountNodesFromAnchorV01 } from './InstructionAccountNode'; import { instructionArgumentNodeFromAnchorV01 } from './InstructionArgumentNode'; import type { GenericsV01 } from './unwrapGenerics'; -export function instructionNodeFromAnchorV01(idl: IdlV01Instruction, generics: GenericsV01): InstructionNode { +export function instructionNodeFromAnchorV01( + idl: IdlV01Instruction, + generics: GenericsV01, + idlTypes: IdlV01TypeDef[] = [], +): InstructionNode { const name = idl.name; let dataArguments = idl.args.map(arg => instructionArgumentNodeFromAnchorV01(arg, generics)); @@ -28,7 +32,13 @@ export function instructionNodeFromAnchorV01(idl: IdlV01Instruction, generics: G const discriminators = [fieldDiscriminatorNode('discriminator')]; return instructionNode({ - accounts: instructionAccountNodesFromAnchorV01(idl.accounts ?? [], dataArguments), + accounts: instructionAccountNodesFromAnchorV01( + idl.accounts ?? [], + dataArguments, + undefined, + idlTypes, + generics, + ), arguments: dataArguments, discriminators, docs: idl.docs ?? [], diff --git a/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts b/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts index fee2730fc..c73db6206 100644 --- a/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts +++ b/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts @@ -2,6 +2,7 @@ import { CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING, CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED, CodamaError, + logWarn, } from '@codama/errors'; import { accountValueNode, @@ -15,17 +16,22 @@ import { pdaSeedValueNode, publicKeyTypeNode, stringTypeNode, + TypeNode, variablePdaSeedNode, } from '@codama/nodes'; import { getBase58Codec } from '@solana/codecs'; -import { IdlV01Seed } from './idl'; +import { IdlV01Field, IdlV01Seed, IdlV01TypeDef } from './idl'; +import { typeNodeFromAnchorV01 } from './typeNodes'; +import type { GenericsV01 } from './unwrapGenerics'; export function pdaSeedNodeFromAnchorV01( seed: IdlV01Seed, instructionArguments: InstructionArgumentNode[], prefix?: string, -): Readonly<{ definition: PdaSeedNode; value?: PdaSeedValueNode }> { + idlTypes: IdlV01TypeDef[] = [], + generics: GenericsV01 = { constArgs: {}, typeArgs: {}, types: {} }, +): Readonly<{ definition: PdaSeedNode; value?: PdaSeedValueNode }> | undefined { const kind = seed.kind; switch (kind) { @@ -34,40 +40,115 @@ export function pdaSeedNodeFromAnchorV01( definition: constantPdaSeedNodeFromBytes('base58', getBase58Codec().decode(new Uint8Array(seed.value))), }; case 'account': { - // Ignore nested paths. - const [accountName] = seed.path.split('.'); + const pathParts = seed.path.split('.'); + const [accountName] = pathParts; const prefixedAccountName = prefix ? `${prefix}_${accountName}` : accountName; + + if (pathParts.length > 1) { + const accountTypeName = seed.account ?? accountName; + const rootType = typeNodeFromAnchorV01({ defined: { name: accountTypeName } }, generics); + const resolved = resolveNestedFieldType(rootType, pathParts.slice(1), idlTypes, generics); + if (!resolved) { + logWarn(`Could not resolve nested account path "${seed.path}" for PDA seed.`); + return undefined; + } + const combinedName = camelCase(`${prefixedAccountName}_${pathParts[pathParts.length - 1]}`); + return { + definition: variablePdaSeedNode(combinedName, resolved), + value: pdaSeedValueNode(combinedName, accountValueNode(prefixedAccountName)), + }; + } + return { definition: variablePdaSeedNode(prefixedAccountName, publicKeyTypeNode()), value: pdaSeedValueNode(prefixedAccountName, accountValueNode(prefixedAccountName)), }; } case 'arg': { - // Ignore nested paths. - const [originalArgumentName] = seed.path.split('.'); - const argumentName = camelCase(originalArgumentName); - const argumentNode = instructionArguments.find(({ name }) => name === argumentName); - if (!argumentNode) { - throw new CodamaError(CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING, { name: originalArgumentName }); + const pathParts = seed.path.split('.'); + const argumentName = camelCase(pathParts.length > 1 ? pathParts[pathParts.length - 1] : pathParts[0]); + + let argumentType: TypeNode; + if (pathParts.length > 1) { + const rootArgName = camelCase(pathParts[0]); + const rootArgNode = instructionArguments.find(({ name }) => name === rootArgName); + if (!rootArgNode) { + throw new CodamaError(CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING, { name: pathParts[0] }); + } + const resolved = resolveNestedFieldType(rootArgNode.type, pathParts.slice(1), idlTypes, generics); + if (!resolved) { + logWarn(`Could not resolve nested arg path "${seed.path}" for PDA seed.`); + return undefined; + } + argumentType = resolved; + } else { + const argumentNode = instructionArguments.find(({ name }) => name === argumentName); + if (!argumentNode) { + throw new CodamaError(CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING, { name: pathParts[0] }); + } + argumentType = argumentNode.type; } - // Anchor uses unprefixed strings for PDA seeds even though the - // argument itself uses a Borsh size-prefixed string. Thus, we - // must recognize this case and convert the type accordingly. - const isBorshString = - isNode(argumentNode.type, 'sizePrefixTypeNode') && - isNode(argumentNode.type.type, 'stringTypeNode') && - argumentNode.type.type.encoding === 'utf8' && - isNode(argumentNode.type.prefix, 'numberTypeNode') && - argumentNode.type.prefix.format === 'u32'; - const argumentType = isBorshString ? stringTypeNode('utf8') : argumentNode.type; + // Anchor uses unprefixed strings for PDA seeds. + if ( + isNode(argumentType, 'sizePrefixTypeNode') && + isNode(argumentType.type, 'stringTypeNode') && + argumentType.type.encoding === 'utf8' && + isNode(argumentType.prefix, 'numberTypeNode') && + argumentType.prefix.format === 'u32' + ) { + argumentType = stringTypeNode('utf8'); + } + + if (pathParts.length > 1) { + return { + definition: variablePdaSeedNode(argumentName, argumentType), + value: pdaSeedValueNode(argumentName, argumentValueNode(argumentName)), + }; + } return { - definition: variablePdaSeedNode(argumentNode.name, argumentType), - value: pdaSeedValueNode(argumentNode.name, argumentValueNode(argumentNode.name)), + definition: variablePdaSeedNode(argumentName, argumentType), + value: pdaSeedValueNode(argumentName, argumentValueNode(argumentName)), }; } default: throw new CodamaError(CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED, { kind }); } } + +function resolveNestedFieldType( + rootType: TypeNode, + fieldPath: string[], + idlTypes: IdlV01TypeDef[], + generics: GenericsV01, +): TypeNode | undefined { + let currentType = rootType; + + for (const fieldName of fieldPath) { + const target = camelCase(fieldName); + + if (isNode(currentType, 'structTypeNode')) { + const field = currentType.fields.find(f => f.name === target); + if (!field) return undefined; + currentType = field.type; + continue; + } + + if (isNode(currentType, 'definedTypeLinkNode')) { + const linkName = currentType.name; + const typeDef = idlTypes.find(t => camelCase(t.name) === linkName); + if (!typeDef || typeDef.type.kind !== 'struct' || !typeDef.type.fields) return undefined; + const { fields } = typeDef.type; + if (!fields.length || typeof fields[0] !== 'object' || !('name' in fields[0])) return undefined; + const field = (fields as IdlV01Field[]).find(f => camelCase(f.name) === target); + if (!field) return undefined; + currentType = typeNodeFromAnchorV01(field.type, generics); + continue; + } + + return undefined; + } + + return currentType; +} diff --git a/packages/nodes-from-anchor/src/v01/ProgramNode.ts b/packages/nodes-from-anchor/src/v01/ProgramNode.ts index b393ab206..5e1e52618 100644 --- a/packages/nodes-from-anchor/src/v01/ProgramNode.ts +++ b/packages/nodes-from-anchor/src/v01/ProgramNode.ts @@ -27,7 +27,7 @@ export function programNodeFromAnchorV01(idl: IdlV01): ProgramNode { definedTypes, errors: errors.map(errorNodeFromAnchorV01), events: events.map(event => eventNodeFromAnchorV01(event, types, generics)), - instructions: instructions.map(instruction => instructionNodeFromAnchorV01(instruction, generics)), + instructions: instructions.map(instruction => instructionNodeFromAnchorV01(instruction, generics, types)), name: idl.metadata.name, origin: 'anchor', publicKey: idl.address, diff --git a/packages/nodes-from-anchor/test/v01/InstructionAccountNode.test.ts b/packages/nodes-from-anchor/test/v01/InstructionAccountNode.test.ts index df82d5614..4078291c0 100644 --- a/packages/nodes-from-anchor/test/v01/InstructionAccountNode.test.ts +++ b/packages/nodes-from-anchor/test/v01/InstructionAccountNode.test.ts @@ -2,6 +2,7 @@ import { accountValueNode, argumentValueNode, constantPdaSeedNodeFromBytes, + definedTypeLinkNode, instructionAccountNode, instructionArgumentNode, numberTypeNode, @@ -10,6 +11,8 @@ import { pdaValueNode, publicKeyTypeNode, publicKeyValueNode, + structFieldTypeNode, + structTypeNode, variablePdaSeedNode, } from '@codama/nodes'; import { expect, test } from 'vitest'; @@ -291,17 +294,75 @@ test('it correctly prefixes PDA seed account references in nested groups', () => ]); }); -test('it ignores PDA default values if at least one seed as a path of length greater than 1', () => { +test('it correctly prefixes nested account seed paths in nested groups', () => { + const nodes = instructionAccountNodesFromAnchorV01( + [ + { + accounts: [ + { name: 'mint', signer: false, writable: false }, + { + name: 'vault', + pda: { + seeds: [{ account: 'mint', kind: 'account', path: 'mint.authority' }], + }, + signer: false, + writable: true, + }, + ], + name: 'tokenProgram', + }, + { + accounts: [ + { name: 'mint', signer: false, writable: false }, + { + name: 'escrow', + pda: { + seeds: [{ account: 'mint', kind: 'account', path: 'mint.authority' }], + }, + signer: false, + writable: true, + }, + ], + name: 'nftProgram', + }, + ], + [], + undefined, + [{ name: 'mint', type: { fields: [{ name: 'authority', type: 'pubkey' }], kind: 'struct' } }], + ); + + expect(nodes).toEqual([ + instructionAccountNode({ isSigner: false, isWritable: false, name: 'tokenProgramMint' }), + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'tokenProgramVault', + seeds: [variablePdaSeedNode('tokenProgramMintAuthority', publicKeyTypeNode())], + }), + [pdaSeedValueNode('tokenProgramMintAuthority', accountValueNode('tokenProgramMint'))], + ), + isSigner: false, + isWritable: true, + name: 'tokenProgramVault', + }), + instructionAccountNode({ isSigner: false, isWritable: false, name: 'nftProgramMint' }), + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'nftProgramEscrow', + seeds: [variablePdaSeedNode('nftProgramMintAuthority', publicKeyTypeNode())], + }), + [pdaSeedValueNode('nftProgramMintAuthority', accountValueNode('nftProgramMint'))], + ), + isSigner: false, + isWritable: true, + name: 'nftProgramEscrow', + }), + ]); +}); + +test('it skips PDA when nested account type cannot be resolved from idlTypes', () => { const nodes = instructionAccountNodesFromAnchorV01( - // [ - // accountNode({ - // data: sizePrefixTypeNode( - // structTypeNode([structFieldTypeNode({ name: 'authority', type: publicKeyTypeNode() })]), - // numberTypeNode('u32'), - // ), - // name: 'mint', - // }), - // ], [ { name: 'somePdaAccount', @@ -330,6 +391,135 @@ test('it ignores PDA default values if at least one seed as a path of length gre ]); }); +test('it resolves PDA seeds with nested arg paths', () => { + const nodes = instructionAccountNodesFromAnchorV01( + [ + { + name: 'my_pda', + pda: { + seeds: [ + { kind: 'const', value: [0, 1, 2, 3] }, + { kind: 'arg', path: 'args.owner' }, + { kind: 'arg', path: 'args.amount' }, + ], + }, + signer: false, + writable: false, + }, + ], + [ + instructionArgumentNode({ + name: 'args', + type: structTypeNode([ + structFieldTypeNode({ name: 'owner', type: publicKeyTypeNode() }), + structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') }), + ]), + }), + ], + ); + + expect(nodes).toEqual([ + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'myPda', + seeds: [ + constantPdaSeedNodeFromBytes('base58', '1Ldp'), + variablePdaSeedNode('owner', publicKeyTypeNode()), + variablePdaSeedNode('amount', numberTypeNode('u64')), + ], + }), + [ + pdaSeedValueNode('owner', argumentValueNode('owner')), + pdaSeedValueNode('amount', argumentValueNode('amount')), + ], + ), + isSigner: false, + isWritable: false, + name: 'myPda', + }), + ]); +}); + +test('it resolves PDA default values when account seeds have nested paths', () => { + const nodes = instructionAccountNodesFromAnchorV01( + [ + { + name: 'somePdaAccount', + pda: { + seeds: [ + { kind: 'arg', path: 'args.owner' }, + { account: 'mint', kind: 'account', path: 'mint.authority' }, + ], + }, + signer: false, + writable: false, + }, + ], + [ + instructionArgumentNode({ + name: 'args', + type: structTypeNode([structFieldTypeNode({ name: 'owner', type: publicKeyTypeNode() })]), + }), + ], + undefined, + [{ name: 'mint', type: { fields: [{ name: 'authority', type: 'pubkey' }], kind: 'struct' } }], + ); + + expect(nodes).toEqual([ + instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'somePdaAccount', + seeds: [ + variablePdaSeedNode('owner', publicKeyTypeNode()), + variablePdaSeedNode('mintAuthority', publicKeyTypeNode()), + ], + }), + [ + pdaSeedValueNode('owner', argumentValueNode('owner')), + pdaSeedValueNode('mintAuthority', accountValueNode('mint')), + ], + ), + isSigner: false, + isWritable: false, + name: 'somePdaAccount', + }), + ]); +}); + +test('it ignores PDA default values when nested arg paths are unresolvable', () => { + const nodes = instructionAccountNodesFromAnchorV01( + [ + { + name: 'somePdaAccount', + pda: { + seeds: [ + { kind: 'const', value: [0, 1, 2, 3] }, + { kind: 'arg', path: 'args.owner' }, + ], + }, + signer: false, + writable: false, + }, + ], + [ + instructionArgumentNode({ + name: 'args', + type: definedTypeLinkNode('UnknownType'), + }), + ], + ); + + expect(nodes).toEqual([ + instructionAccountNode({ + isSigner: false, + isWritable: false, + name: 'somePdaAccount', + }), + ]); +}); + test('it handles PDAs with a constant program id', () => { const nodes = instructionAccountNodesFromAnchorV01( [ @@ -406,17 +596,99 @@ test('it handles PDAs with a program id that points to another account', () => { ]); }); -test.skip('it handles account data paths of length 2', () => { +test('it ignores PDA default values when program seed has a nested path', () => { + const nodes = instructionAccountNodesFromAnchorV01( + [ + { + name: 'my_pda', + pda: { + program: { kind: 'arg', path: 'config.programId' }, + seeds: [{ kind: 'const', value: [0, 1, 2, 3] }], + }, + signer: false, + writable: false, + }, + ], + [ + instructionArgumentNode({ + name: 'config', + type: structTypeNode([structFieldTypeNode({ name: 'programId', type: publicKeyTypeNode() })]), + }), + ], + ); + + expect(nodes).toEqual([ + instructionAccountNode({ + isSigner: false, + isWritable: false, + name: 'myPda', + }), + ]); +}); + +test('it skips PDA default when a seed references the account itself', () => { + const nodes = instructionAccountNodesFromAnchorV01( + [ + { + name: 'vault', + pda: { + seeds: [ + { kind: 'const', value: [1, 2, 3] }, + { kind: 'account', path: 'vault' }, + ], + }, + signer: false, + writable: false, + }, + { + name: 'guard', + pda: { + seeds: [ + { kind: 'const', value: [1, 2, 3] }, + { account: 'GuardV1', kind: 'account', path: 'guard.mint' }, + ], + }, + signer: false, + writable: false, + }, + { + name: 'my_guard', + pda: { + seeds: [ + { kind: 'const', value: [1, 2, 3] }, + { account: 'GuardV1', kind: 'account', path: 'my_guard.mint' }, + ], + }, + signer: false, + writable: false, + }, + ], + [], + undefined, + [{ name: 'GuardV1', type: { fields: [{ name: 'mint', type: 'pubkey' }], kind: 'struct' } }], + ); + + expect(nodes).toEqual([ + instructionAccountNode({ + isSigner: false, + isWritable: false, + name: 'vault', + }), + instructionAccountNode({ + isSigner: false, + isWritable: false, + name: 'guard', + }), + instructionAccountNode({ + isSigner: false, + isWritable: false, + name: 'myGuard', + }), + ]); +}); + +test('it handles account data paths of length 2', () => { const nodes = instructionAccountNodesFromAnchorV01( - // [ - // accountNode({ - // data: sizePrefixTypeNode( - // structTypeNode([structFieldTypeNode({ name: 'authority', type: publicKeyTypeNode() })]), - // numberTypeNode('u32'), - // ), - // name: 'mint', - // }), - // ], [ { name: 'somePdaAccount', @@ -434,6 +706,8 @@ test.skip('it handles account data paths of length 2', () => { }, ], [], + undefined, + [{ name: 'mint', type: { fields: [{ name: 'authority', type: 'pubkey' }], kind: 'struct' } }], ); expect(nodes).toEqual([ @@ -443,7 +717,7 @@ test.skip('it handles account data paths of length 2', () => { name: 'somePdaAccount', seeds: [variablePdaSeedNode('mintAuthority', publicKeyTypeNode())], }), - [], + [pdaSeedValueNode('mintAuthority', accountValueNode('mint'))], ), isSigner: false, isWritable: false, diff --git a/packages/nodes-from-anchor/test/v01/InstructionNode.test.ts b/packages/nodes-from-anchor/test/v01/InstructionNode.test.ts index 2d328a6bd..7cd389fcb 100644 --- a/packages/nodes-from-anchor/test/v01/InstructionNode.test.ts +++ b/packages/nodes-from-anchor/test/v01/InstructionNode.test.ts @@ -34,23 +34,13 @@ test('it creates instruction nodes', () => { name: 'mintTokens', }, generics, + [{ name: 'Distribution', type: { fields: [{ name: 'group_mint', type: 'pubkey' }], kind: 'struct' } }], ); expect(node).toEqual( instructionNode({ accounts: [ instructionAccountNode({ - // 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'))], - // ), isSigner: false, isWritable: true, name: 'distribution', diff --git a/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts b/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts index 6948bbef0..95a447ab0 100644 --- a/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts +++ b/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts @@ -2,12 +2,15 @@ import { accountValueNode, argumentValueNode, constantPdaSeedNodeFromBytes, + definedTypeLinkNode, instructionArgumentNode, numberTypeNode, pdaSeedValueNode, publicKeyTypeNode, sizePrefixTypeNode, stringTypeNode, + structFieldTypeNode, + structTypeNode, variablePdaSeedNode, } from '@codama/nodes'; import { expect, test } from 'vitest'; @@ -17,15 +20,15 @@ import { pdaSeedNodeFromAnchorV01 } from '../../src'; test('it creates a PdaSeedNode from a const Anchor seed', () => { const nodes = pdaSeedNodeFromAnchorV01({ kind: 'const', value: [11, 57, 246, 240] }, []); - expect(nodes.definition).toEqual(constantPdaSeedNodeFromBytes('base58', 'HeLLo')); - expect(nodes.value).toBeUndefined(); + expect(nodes!.definition).toEqual(constantPdaSeedNodeFromBytes('base58', 'HeLLo')); + expect(nodes!.value).toBeUndefined(); }); test('it creates a PdaSeedNode from an account Anchor seed', () => { const nodes = pdaSeedNodeFromAnchorV01({ kind: 'account', path: 'authority' }, []); - expect(nodes.definition).toEqual(variablePdaSeedNode('authority', publicKeyTypeNode())); - expect(nodes.value).toEqual(pdaSeedValueNode('authority', accountValueNode('authority'))); + expect(nodes!.definition).toEqual(variablePdaSeedNode('authority', publicKeyTypeNode())); + expect(nodes!.value).toEqual(pdaSeedValueNode('authority', accountValueNode('authority'))); }); test('it creates a PdaSeedNode from an arg Anchor seed', () => { @@ -33,8 +36,70 @@ test('it creates a PdaSeedNode from an arg Anchor seed', () => { instructionArgumentNode({ name: 'capacity', type: numberTypeNode('u64') }), ]); - expect(nodes.definition).toEqual(variablePdaSeedNode('capacity', numberTypeNode('u64'))); - expect(nodes.value).toEqual(pdaSeedValueNode('capacity', argumentValueNode('capacity'))); + expect(nodes!.definition).toEqual(variablePdaSeedNode('capacity', numberTypeNode('u64'))); + expect(nodes!.value).toEqual(pdaSeedValueNode('capacity', argumentValueNode('capacity'))); +}); + +test('it resolves nested arg path from inline struct type', () => { + const nodes = pdaSeedNodeFromAnchorV01({ kind: 'arg', path: 'args.owner' }, [ + instructionArgumentNode({ + name: 'args', + type: structTypeNode([ + structFieldTypeNode({ name: 'owner', type: publicKeyTypeNode() }), + structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') }), + ]), + }), + ]); + + expect(nodes!.definition).toEqual(variablePdaSeedNode('owner', publicKeyTypeNode())); + expect(nodes!.value).toEqual(pdaSeedValueNode('owner', argumentValueNode('owner'))); +}); + +test('it resolves nested arg path from defined type link', () => { + const nodes = pdaSeedNodeFromAnchorV01( + { kind: 'arg', path: 'args.amount' }, + [instructionArgumentNode({ name: 'args', type: definedTypeLinkNode('MyArgs') })], + undefined, + [{ name: 'MyArgs', type: { fields: [{ name: 'amount', type: 'u64' }], kind: 'struct' } }], + ); + + expect(nodes!.definition).toEqual(variablePdaSeedNode('amount', numberTypeNode('u64'))); + expect(nodes!.value).toEqual(pdaSeedValueNode('amount', argumentValueNode('amount'))); +}); + +test('it returns undefined for unresolvable nested arg type', () => { + const result = pdaSeedNodeFromAnchorV01({ kind: 'arg', path: 'args.owner' }, [ + instructionArgumentNode({ name: 'args', type: definedTypeLinkNode('UnknownType') }), + ]); + + expect(result).toBeUndefined(); +}); + +test('it throws for nested arg path when root argument is missing', () => { + expect(() => pdaSeedNodeFromAnchorV01({ kind: 'arg', path: 'args.owner' }, [])).toThrow(); +}); + +test('it resolves nested account path from type def', () => { + const nodes = pdaSeedNodeFromAnchorV01( + { account: 'Mint', kind: 'account', path: 'mint.authority' }, + [], + undefined, + [{ name: 'Mint', type: { fields: [{ name: 'authority', type: 'pubkey' }], kind: 'struct' } }], + ); + + expect(nodes!.definition).toEqual(variablePdaSeedNode('mintAuthority', publicKeyTypeNode())); + expect(nodes!.value).toEqual(pdaSeedValueNode('mintAuthority', accountValueNode('mint'))); +}); + +test('it returns undefined for unresolvable nested account path', () => { + const result = pdaSeedNodeFromAnchorV01( + { account: 'UnknownType', kind: 'account', path: 'mint.authority' }, + [], + undefined, + [], + ); + + expect(result).toBeUndefined(); }); test('it removes the string prefix from arg Anchor seeds', () => { @@ -45,6 +110,6 @@ test('it removes the string prefix from arg Anchor seeds', () => { }), ]); - expect(nodes.definition).toEqual(variablePdaSeedNode('identifier', stringTypeNode('utf8'))); - expect(nodes.value).toEqual(pdaSeedValueNode('identifier', argumentValueNode('identifier'))); + expect(nodes!.definition).toEqual(variablePdaSeedNode('identifier', stringTypeNode('utf8'))); + expect(nodes!.value).toEqual(pdaSeedValueNode('identifier', argumentValueNode('identifier'))); }); From 09b1602d7b5d49fc72350a4bdde72fd306a17140 Mon Sep 17 00:00:00 2001 From: ioxde <228087182+ioxde@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:44:01 -0700 Subject: [PATCH 3/7] Fix dynamic-client tests --- .../test/programs/anchor/tests/blog.test.ts | 8 +- .../programs/anchor/tests/example.test.ts | 10 ++- .../test/programs/idls/blog-idl.json | 67 ++++++++++++++- .../test/programs/idls/example-idl.json | 84 +++++++++++++------ 4 files changed, 133 insertions(+), 36 deletions(-) diff --git a/packages/dynamic-client/test/programs/anchor/tests/blog.test.ts b/packages/dynamic-client/test/programs/anchor/tests/blog.test.ts index dea387a02..cdeb1cf32 100644 --- a/packages/dynamic-client/test/programs/anchor/tests/blog.test.ts +++ b/packages/dynamic-client/test/programs/anchor/tests/blog.test.ts @@ -108,8 +108,8 @@ describe('blog', () => { const [profilePda] = await programClient.pdas.profile({ authority: payer }); - // Create post (manual PDA — Codama can't express profile.post_count dependency) - const [postPda] = await programClient.pdas.post({ postId: 0, profile: profilePda }); + // Create post (manual PDA — uses profile.post_count nested seed) + const [postPda] = await programClient.pdas.post({ profile: profilePda, profilePostCount: 0 }); const createPostIx = await programClient.methods .createPost({ content: 'Original content', title: 'Original' }) .accounts({ authority: payer, post: postPda }) @@ -139,7 +139,7 @@ describe('blog', () => { await ctx.sendInstruction(createProfileIx, [payer]); const [profilePda] = await programClient.pdas.profile({ authority: payer }); - const [postPda] = await programClient.pdas.post({ postId: 0, profile: profilePda }); + const [postPda] = await programClient.pdas.post({ profile: profilePda, profilePostCount: 0 }); const createPostIx = await programClient.methods .createPost({ content: 'World', title: 'Hello' }) @@ -175,7 +175,7 @@ describe('blog', () => { await ctx.sendInstruction(createProfileIx, [payer]); const [profilePda] = await programClient.pdas.profile({ authority: payer }); - const [postPda] = await programClient.pdas.post({ postId: 0, profile: profilePda }); + const [postPda] = await programClient.pdas.post({ profile: profilePda, profilePostCount: 0 }); const createPostIx = await programClient.methods .createPost({ content: 'There', title: 'Hi' }) diff --git a/packages/dynamic-client/test/programs/anchor/tests/example.test.ts b/packages/dynamic-client/test/programs/anchor/tests/example.test.ts index 561ad07aa..f768d1faf 100644 --- a/packages/dynamic-client/test/programs/anchor/tests/example.test.ts +++ b/packages/dynamic-client/test/programs/anchor/tests/example.test.ts @@ -241,10 +241,12 @@ describe('anchor-example: commonIxs', () => { }); describe('Circular Dependency Detection', () => { - test('SelfReferencePdaIx: should throw AccountError for A->A cycle', async () => { - await expect( - programClient.methods.selfReferencePda().accounts({ signer: payer }).instruction(), - ).rejects.toThrow(/Circular dependency detected: \[recursive -> recursive\]/); + test('SelfReferencePdaIx: self-referential seed is dropped, requires manual account', async () => { + const ix = await programClient.methods + .selfReferencePda() + .accounts({ recursive: payer, signer: payer }) + .instruction(); + expect(ix).toBeDefined(); }); test('TwoNodeCyclePdaIx: should throw AccountError for A->B->A pattern in two-node cycle', async () => { diff --git a/packages/dynamic-client/test/programs/idls/blog-idl.json b/packages/dynamic-client/test/programs/idls/blog-idl.json index 9f91b658c..dc70f4b19 100644 --- a/packages/dynamic-client/test/programs/idls/blog-idl.json +++ b/packages/dynamic-client/test/programs/idls/blog-idl.json @@ -1163,7 +1163,32 @@ "isWritable": true, "isSigner": false, "isOptional": false, - "docs": [] + "docs": [], + "defaultValue": { + "kind": "pdaValueNode", + "pda": { + "kind": "pdaLinkNode", + "name": "post" + }, + "seeds": [ + { + "kind": "pdaSeedValueNode", + "name": "profile", + "value": { + "kind": "accountValueNode", + "name": "profile" + } + }, + { + "kind": "pdaSeedValueNode", + "name": "profilePostCount", + "value": { + "kind": "accountValueNode", + "name": "profile" + } + } + ] + } }, { "kind": "instructionAccountNode", @@ -1607,7 +1632,7 @@ "kind": "pdaValueNode", "pda": { "kind": "pdaLinkNode", - "name": "post" + "name": "updatePostPost" }, "seeds": [ { @@ -1886,6 +1911,42 @@ } ] }, + { + "kind": "pdaNode", + "name": "post", + "docs": [], + "seeds": [ + { + "kind": "constantPdaSeedNode", + "type": { + "kind": "bytesTypeNode" + }, + "value": { + "kind": "bytesValueNode", + "data": "3sh3oZ", + "encoding": "base58" + } + }, + { + "kind": "variablePdaSeedNode", + "name": "profile", + "docs": [], + "type": { + "kind": "publicKeyTypeNode" + } + }, + { + "kind": "variablePdaSeedNode", + "name": "profilePostCount", + "docs": [], + "type": { + "kind": "numberTypeNode", + "format": "u64", + "endian": "le" + } + } + ] + }, { "kind": "pdaNode", "name": "reaction", @@ -1966,7 +2027,7 @@ }, { "kind": "pdaNode", - "name": "post", + "name": "updatePostPost", "docs": [], "seeds": [ { diff --git a/packages/dynamic-client/test/programs/idls/example-idl.json b/packages/dynamic-client/test/programs/idls/example-idl.json index f4fc4147d..44833f41d 100644 --- a/packages/dynamic-client/test/programs/idls/example-idl.json +++ b/packages/dynamic-client/test/programs/idls/example-idl.json @@ -556,7 +556,40 @@ "isWritable": true, "isSigner": false, "isOptional": false, - "docs": [] + "docs": [], + "defaultValue": { + "kind": "pdaValueNode", + "pda": { + "kind": "pdaLinkNode", + "name": "nestedExampleAccount" + }, + "seeds": [ + { + "kind": "pdaSeedValueNode", + "name": "pubkey", + "value": { + "kind": "argumentValueNode", + "name": "pubkey" + } + }, + { + "kind": "pdaSeedValueNode", + "name": "seedEnum", + "value": { + "kind": "argumentValueNode", + "name": "seedEnum" + } + }, + { + "kind": "pdaSeedValueNode", + "name": "innerStructSeedEnum", + "value": { + "kind": "argumentValueNode", + "name": "innerStructSeedEnum" + } + } + ] + } }, { "kind": "instructionAccountNode", @@ -784,24 +817,7 @@ "isWritable": true, "isSigner": false, "isOptional": false, - "docs": [], - "defaultValue": { - "kind": "pdaValueNode", - "pda": { - "kind": "pdaLinkNode", - "name": "recursive" - }, - "seeds": [ - { - "kind": "pdaSeedValueNode", - "name": "recursive", - "value": { - "kind": "accountValueNode", - "name": "recursive" - } - } - ] - } + "docs": [] }, { "kind": "instructionAccountNode", @@ -1780,7 +1796,7 @@ }, { "kind": "pdaNode", - "name": "newAccount", + "name": "nestedExampleAccount", "docs": [], "seeds": [ { @@ -1790,23 +1806,41 @@ }, "value": { "kind": "bytesValueNode", - "data": "3x5dmD", + "data": "WxqkpQv6EzD7FjkQ2ENw5KvgMqrLQj", "encoding": "base58" } }, { "kind": "variablePdaSeedNode", - "name": "signer", + "name": "pubkey", "docs": [], "type": { "kind": "publicKeyTypeNode" } + }, + { + "kind": "variablePdaSeedNode", + "name": "seedEnum", + "docs": [], + "type": { + "kind": "definedTypeLinkNode", + "name": "seedEnum" + } + }, + { + "kind": "variablePdaSeedNode", + "name": "innerStructSeedEnum", + "docs": [], + "type": { + "kind": "definedTypeLinkNode", + "name": "seedEnum" + } } ] }, { "kind": "pdaNode", - "name": "recursive", + "name": "newAccount", "docs": [], "seeds": [ { @@ -1816,13 +1850,13 @@ }, "value": { "kind": "bytesValueNode", - "data": "2TTMxGdnk9y5E", + "data": "3x5dmD", "encoding": "base58" } }, { "kind": "variablePdaSeedNode", - "name": "recursive", + "name": "signer", "docs": [], "type": { "kind": "publicKeyTypeNode" From a37264c9c2d6c2eb71ef67f976063f2c86cc816f Mon Sep 17 00:00:00 2001 From: ioxde <228087182+ioxde@users.noreply.github.com> Date: Tue, 14 Apr 2026 16:52:30 -0700 Subject: [PATCH 4/7] Fix duplicate seed names for nested arg PDA seeds * found while rebasing onto main --- .../nodes-from-anchor/src/v01/PdaSeedNode.ts | 2 +- .../test/v01/pdaSeedNode.test.ts | 21 +++++++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts b/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts index c73db6206..00b2052b9 100644 --- a/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts +++ b/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts @@ -66,7 +66,7 @@ export function pdaSeedNodeFromAnchorV01( } case 'arg': { const pathParts = seed.path.split('.'); - const argumentName = camelCase(pathParts.length > 1 ? pathParts[pathParts.length - 1] : pathParts[0]); + const argumentName = camelCase(pathParts.length > 1 ? pathParts.slice(1).join('_') : pathParts[0]); let argumentType: TypeNode; if (pathParts.length > 1) { diff --git a/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts b/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts index 95a447ab0..0410b2afa 100644 --- a/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts +++ b/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts @@ -67,6 +67,27 @@ test('it resolves nested arg path from defined type link', () => { expect(nodes!.value).toEqual(pdaSeedValueNode('amount', argumentValueNode('amount'))); }); +test('it uses full nested path to avoid name collisions for deeply nested arg seeds', () => { + const instructionArgs = [ + instructionArgumentNode({ + name: 'input', + type: structTypeNode([ + structFieldTypeNode({ name: 'seedEnum', type: numberTypeNode('u8') }), + structFieldTypeNode({ + name: 'innerStruct', + type: structTypeNode([structFieldTypeNode({ name: 'seedEnum', type: numberTypeNode('u8') })]), + }), + ]), + }), + ]; + + const shallow = pdaSeedNodeFromAnchorV01({ kind: 'arg', path: 'input.seed_enum' }, instructionArgs); + const deep = pdaSeedNodeFromAnchorV01({ kind: 'arg', path: 'input.inner_struct.seed_enum' }, instructionArgs); + + expect(shallow?.definition).toEqual(variablePdaSeedNode('seedEnum', numberTypeNode('u8'))); + expect(deep?.definition).toEqual(variablePdaSeedNode('innerStructSeedEnum', numberTypeNode('u8'))); +}); + test('it returns undefined for unresolvable nested arg type', () => { const result = pdaSeedNodeFromAnchorV01({ kind: 'arg', path: 'args.owner' }, [ instructionArgumentNode({ name: 'args', type: definedTypeLinkNode('UnknownType') }), From 10ab1a84e7e6e39e9fd5b3280b7f44e43076ddb1 Mon Sep 17 00:00:00 2001 From: ioxde <228087182+ioxde@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:56:20 -0700 Subject: [PATCH 5/7] Resolve nested program seed paths in PDA extraction --- .../src/v01/InstructionAccountNode.ts | 94 +++++++++---------- .../test/v01/InstructionAccountNode.test.ts | 10 +- 2 files changed, 53 insertions(+), 51 deletions(-) diff --git a/packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts b/packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts index 93c3f831f..b23cda645 100644 --- a/packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts +++ b/packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts @@ -88,61 +88,55 @@ export function instructionAccountNodeFromAnchorV01( if (idl.address) { defaultValue = publicKeyValueNode(idl.address, name); } else if (idl.pda) { - const hasNestedProgramPath = - idl.pda.program != null && 'path' in idl.pda.program && idl.pda.program.path.includes('.'); - if (hasNestedProgramPath) { - logWarn(`Skipping PDA for account "${name}": program seed uses a nested path that cannot be resolved.`); - } else { - const seedResults = idl.pda.seeds.map(seed => - pdaSeedNodeFromAnchorV01(seed, instructionArguments, prefix, idlTypes, generics), - ); + const seedResults = idl.pda.seeds.map(seed => + pdaSeedNodeFromAnchorV01(seed, instructionArguments, prefix, idlTypes, generics), + ); - if (seedResults.every((r): r is NonNullable => r != null)) { - const seedDefinitions = seedResults.map(r => r.definition); - const seedValues = seedResults.flatMap(r => (r.value ? [r.value] : [])); + if (seedResults.every((r): r is NonNullable => r != null)) { + const seedDefinitions = seedResults.map(r => r.definition); + const seedValues = seedResults.flatMap(r => (r.value ? [r.value] : [])); - let programId: string | undefined; - let programIdValue: AccountValueNode | ArgumentValueNode | undefined; - if (idl.pda.program !== undefined) { - const result = pdaSeedNodeFromAnchorV01( - idl.pda.program, - instructionArguments, - prefix, - idlTypes, - generics, - ); - if (!result) { - logWarn(`Skipping PDA for account "${name}": program seed could not be resolved.`); - return instructionAccountNode({ defaultValue, docs, isOptional, isSigner, isWritable, name }); - } - if ( - isNode(result.definition, 'constantPdaSeedNode') && - isNode(result.definition.value, 'bytesValueNode') && - result.definition.value.encoding === 'base58' - ) { - programId = result.definition.value.data; - } else if (result.value && isNode(result.value.value, ['accountValueNode', 'argumentValueNode'])) { - programIdValue = result.value.value; - } + let programId: string | undefined; + let programIdValue: AccountValueNode | ArgumentValueNode | undefined; + if (idl.pda.program !== undefined) { + const result = pdaSeedNodeFromAnchorV01( + idl.pda.program, + instructionArguments, + prefix, + idlTypes, + generics, + ); + if (!result) { + logWarn(`Skipping PDA for account "${name}": program seed could not be resolved.`); + return instructionAccountNode({ defaultValue, docs, isOptional, isSigner, isWritable, name }); } - - const camelName = camelCase(name); - const isSelfReferential = - seedValues.some(sv => isNode(sv.value, 'accountValueNode') && sv.value.name === camelName) || - (programIdValue != null && - isNode(programIdValue, 'accountValueNode') && - programIdValue.name === camelName); - if (isSelfReferential) { - logWarn(`Skipping PDA for account "${name}": a seed references the account itself.`); - } - if (!isSelfReferential) { - defaultValue = pdaValueNode( - pdaNode({ name, programId, seeds: seedDefinitions }), - seedValues, - programIdValue, - ); + if ( + isNode(result.definition, 'constantPdaSeedNode') && + isNode(result.definition.value, 'bytesValueNode') && + result.definition.value.encoding === 'base58' + ) { + programId = result.definition.value.data; + } else if (result.value && isNode(result.value.value, ['accountValueNode', 'argumentValueNode'])) { + programIdValue = result.value.value; } } + + const camelName = camelCase(name); + const isSelfReferential = + seedValues.some(sv => isNode(sv.value, 'accountValueNode') && sv.value.name === camelName) || + (programIdValue != null && + isNode(programIdValue, 'accountValueNode') && + programIdValue.name === camelName); + if (isSelfReferential) { + logWarn(`Skipping PDA for account "${name}": a seed references the account itself.`); + } + if (!isSelfReferential) { + defaultValue = pdaValueNode( + pdaNode({ name, programId, seeds: seedDefinitions }), + seedValues, + programIdValue, + ); + } } } diff --git a/packages/nodes-from-anchor/test/v01/InstructionAccountNode.test.ts b/packages/nodes-from-anchor/test/v01/InstructionAccountNode.test.ts index 4078291c0..704a7e2d7 100644 --- a/packages/nodes-from-anchor/test/v01/InstructionAccountNode.test.ts +++ b/packages/nodes-from-anchor/test/v01/InstructionAccountNode.test.ts @@ -596,7 +596,7 @@ test('it handles PDAs with a program id that points to another account', () => { ]); }); -test('it ignores PDA default values when program seed has a nested path', () => { +test('it resolves PDA default values when program seed has a nested path', () => { const nodes = instructionAccountNodesFromAnchorV01( [ { @@ -619,6 +619,14 @@ test('it ignores PDA default values when program seed has a nested path', () => expect(nodes).toEqual([ instructionAccountNode({ + defaultValue: pdaValueNode( + pdaNode({ + name: 'myPda', + seeds: [constantPdaSeedNodeFromBytes('base58', '1Ldp')], + }), + [], + argumentValueNode('programId'), + ), isSigner: false, isWritable: false, name: 'myPda', From b0c4d41b1b39a85eca83c93349afa1b9035b3568 Mon Sep 17 00:00:00 2001 From: ioxde <228087182+ioxde@users.noreply.github.com> Date: Tue, 14 Apr 2026 21:43:54 -0700 Subject: [PATCH 6/7] Handle tupleTypeNode in nested PDA seed path resolution --- .../nodes-from-anchor/src/v01/PdaSeedNode.ts | 23 +++++----- .../test/v01/pdaSeedNode.test.ts | 45 +++++++++++++++++++ 2 files changed, 58 insertions(+), 10 deletions(-) diff --git a/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts b/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts index 00b2052b9..acaefcfe3 100644 --- a/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts +++ b/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts @@ -21,7 +21,7 @@ import { } from '@codama/nodes'; import { getBase58Codec } from '@solana/codecs'; -import { IdlV01Field, IdlV01Seed, IdlV01TypeDef } from './idl'; +import { IdlV01Seed, IdlV01TypeDef } from './idl'; import { typeNodeFromAnchorV01 } from './typeNodes'; import type { GenericsV01 } from './unwrapGenerics'; @@ -128,6 +128,14 @@ function resolveNestedFieldType( for (const fieldName of fieldPath) { const target = camelCase(fieldName); + // Resolve type links before handling struct/tuple field lookup. + if (isNode(currentType, 'definedTypeLinkNode')) { + const linkName = currentType.name; + const typeDef = idlTypes.find(t => camelCase(t.name) === linkName); + if (!typeDef) return undefined; + currentType = typeNodeFromAnchorV01(typeDef.type, generics); + } + if (isNode(currentType, 'structTypeNode')) { const field = currentType.fields.find(f => f.name === target); if (!field) return undefined; @@ -135,15 +143,10 @@ function resolveNestedFieldType( continue; } - if (isNode(currentType, 'definedTypeLinkNode')) { - const linkName = currentType.name; - const typeDef = idlTypes.find(t => camelCase(t.name) === linkName); - if (!typeDef || typeDef.type.kind !== 'struct' || !typeDef.type.fields) return undefined; - const { fields } = typeDef.type; - if (!fields.length || typeof fields[0] !== 'object' || !('name' in fields[0])) return undefined; - const field = (fields as IdlV01Field[]).find(f => camelCase(f.name) === target); - if (!field) return undefined; - currentType = typeNodeFromAnchorV01(field.type, generics); + if (isNode(currentType, 'tupleTypeNode')) { + const index = Number(fieldName); + if (Number.isNaN(index) || index < 0 || index >= currentType.items.length) return undefined; + currentType = currentType.items[index]; continue; } diff --git a/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts b/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts index 0410b2afa..9c2543fee 100644 --- a/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts +++ b/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts @@ -11,6 +11,7 @@ import { stringTypeNode, structFieldTypeNode, structTypeNode, + tupleTypeNode, variablePdaSeedNode, } from '@codama/nodes'; import { expect, test } from 'vitest'; @@ -123,6 +124,50 @@ test('it returns undefined for unresolvable nested account path', () => { expect(result).toBeUndefined(); }); +test('it resolves nested arg path through inline tuple type', () => { + const nodes = pdaSeedNodeFromAnchorV01({ kind: 'arg', path: 'foo.0' }, [ + instructionArgumentNode({ + name: 'foo', + type: tupleTypeNode([publicKeyTypeNode(), numberTypeNode('u64')]), + }), + ]); + + expect(nodes?.definition).toEqual(variablePdaSeedNode('0', publicKeyTypeNode())); + expect(nodes?.value).toEqual(pdaSeedValueNode('0', argumentValueNode('0'))); +}); + +test('it resolves nested path through tuple then struct (foo.0.bar)', () => { + const nodes = pdaSeedNodeFromAnchorV01({ kind: 'arg', path: 'foo.0.bar' }, [ + instructionArgumentNode({ + name: 'foo', + type: tupleTypeNode([structTypeNode([structFieldTypeNode({ name: 'bar', type: numberTypeNode('u8') })])]), + }), + ]); + + expect(nodes?.definition).toEqual(variablePdaSeedNode('0Bar', numberTypeNode('u8'))); + expect(nodes?.value).toEqual(pdaSeedValueNode('0Bar', argumentValueNode('0Bar'))); +}); + +test('it returns undefined for out-of-bounds tuple index', () => { + const result = pdaSeedNodeFromAnchorV01({ kind: 'arg', path: 'foo.5' }, [ + instructionArgumentNode({ + name: 'foo', + type: tupleTypeNode([publicKeyTypeNode()]), + }), + ]); + + expect(result).toBeUndefined(); +}); + +test('it resolves nested account path through IDL tuple type def', () => { + const nodes = pdaSeedNodeFromAnchorV01({ account: 'Pair', kind: 'account', path: 'pair.0' }, [], undefined, [ + { name: 'Pair', type: { fields: ['pubkey', 'u64'], kind: 'struct' } }, + ]); + + expect(nodes?.definition).toEqual(variablePdaSeedNode('pair0', publicKeyTypeNode())); + expect(nodes?.value).toEqual(pdaSeedValueNode('pair0', accountValueNode('pair'))); +}); + test('it removes the string prefix from arg Anchor seeds', () => { const nodes = pdaSeedNodeFromAnchorV01({ kind: 'arg', path: 'identifier' }, [ instructionArgumentNode({ From 48b8986bbd044e67fae81040514f21a06f3670e6 Mon Sep 17 00:00:00 2001 From: ioxde <228087182+ioxde@users.noreply.github.com> Date: Tue, 14 Apr 2026 18:38:02 -0700 Subject: [PATCH 7/7] Address PR feedback --- .../src/v01/InstructionAccountNode.ts | 134 +++++++++++------- .../src/v01/InstructionNode.ts | 2 +- .../nodes-from-anchor/src/v01/PdaSeedNode.ts | 21 +-- .../nodes-from-anchor/src/v01/ProgramNode.ts | 2 +- .../test/v01/InstructionNode.test.ts | 3 +- .../test/v01/pdaSeedNode.test.ts | 28 ++-- 6 files changed, 107 insertions(+), 83 deletions(-) diff --git a/packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts b/packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts index b23cda645..2a1d06751 100644 --- a/packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts +++ b/packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts @@ -8,13 +8,15 @@ import { InstructionArgumentNode, isNode, pdaNode, + PdaSeedNode, + PdaSeedValueNode, PdaValueNode, pdaValueNode, PublicKeyValueNode, publicKeyValueNode, } from '@codama/nodes'; -import { IdlV01InstructionAccount, IdlV01InstructionAccountItem, IdlV01TypeDef } from './idl'; +import { IdlV01InstructionAccount, IdlV01InstructionAccountItem, IdlV01Pda, IdlV01Seed, IdlV01TypeDef } from './idl'; import { pdaSeedNodeFromAnchorV01 } from './PdaSeedNode'; import type { GenericsV01 } from './unwrapGenerics'; @@ -84,60 +86,10 @@ export function instructionAccountNodeFromAnchorV01( const isWritable = idl.writable ?? false; const name = prefix ? `${prefix}_${idl.name ?? ''}` : (idl.name ?? ''); let defaultValue: PdaValueNode | PublicKeyValueNode | undefined; - if (idl.address) { defaultValue = publicKeyValueNode(idl.address, name); } else if (idl.pda) { - const seedResults = idl.pda.seeds.map(seed => - pdaSeedNodeFromAnchorV01(seed, instructionArguments, prefix, idlTypes, generics), - ); - - if (seedResults.every((r): r is NonNullable => r != null)) { - const seedDefinitions = seedResults.map(r => r.definition); - const seedValues = seedResults.flatMap(r => (r.value ? [r.value] : [])); - - let programId: string | undefined; - let programIdValue: AccountValueNode | ArgumentValueNode | undefined; - if (idl.pda.program !== undefined) { - const result = pdaSeedNodeFromAnchorV01( - idl.pda.program, - instructionArguments, - prefix, - idlTypes, - generics, - ); - if (!result) { - logWarn(`Skipping PDA for account "${name}": program seed could not be resolved.`); - return instructionAccountNode({ defaultValue, docs, isOptional, isSigner, isWritable, name }); - } - if ( - isNode(result.definition, 'constantPdaSeedNode') && - isNode(result.definition.value, 'bytesValueNode') && - result.definition.value.encoding === 'base58' - ) { - programId = result.definition.value.data; - } else if (result.value && isNode(result.value.value, ['accountValueNode', 'argumentValueNode'])) { - programIdValue = result.value.value; - } - } - - const camelName = camelCase(name); - const isSelfReferential = - seedValues.some(sv => isNode(sv.value, 'accountValueNode') && sv.value.name === camelName) || - (programIdValue != null && - isNode(programIdValue, 'accountValueNode') && - programIdValue.name === camelName); - if (isSelfReferential) { - logWarn(`Skipping PDA for account "${name}": a seed references the account itself.`); - } - if (!isSelfReferential) { - defaultValue = pdaValueNode( - pdaNode({ name, programId, seeds: seedDefinitions }), - seedValues, - programIdValue, - ); - } - } + defaultValue = resolvePdaDefaultValue(idl.pda, name, instructionArguments, prefix, idlTypes, generics); } return instructionAccountNode({ @@ -149,3 +101,81 @@ export function instructionAccountNodeFromAnchorV01( name, }); } + +function resolvePdaDefaultValue( + pda: IdlV01Pda, + name: string, + instructionArguments: InstructionArgumentNode[], + prefix: string | undefined, + idlTypes: IdlV01TypeDef[], + generics: GenericsV01, +): PdaValueNode | undefined { + const seeds = resolveSeeds(pda.seeds, instructionArguments, prefix, idlTypes, generics); + if (!seeds) return undefined; + + let programId: string | undefined; + let programValue: AccountValueNode | ArgumentValueNode | undefined; + if (pda.program) { + const result = resolveProgramSeed(pda.program, name, instructionArguments, prefix, idlTypes, generics); + if (!result) return undefined; + programId = result.id; + programValue = result.value; + } + + const camelName = camelCase(name); + const isSelfReferential = + seeds.values.some(sv => isNode(sv.value, 'accountValueNode') && sv.value.name === camelName) || + (programValue != null && isNode(programValue, 'accountValueNode') && programValue.name === camelName); + if (isSelfReferential) { + logWarn(`Skipping PDA for account "${name}": a seed references the account itself.`); + return undefined; + } + + return pdaValueNode(pdaNode({ name, programId, seeds: seeds.definitions }), seeds.values, programValue); +} + +function resolveSeeds( + seeds: IdlV01Seed[], + instructionArguments: InstructionArgumentNode[], + prefix: string | undefined, + idlTypes: IdlV01TypeDef[], + generics: GenericsV01, +): { definitions: PdaSeedNode[]; values: PdaSeedValueNode[] } | undefined { + const results = seeds.map(seed => pdaSeedNodeFromAnchorV01(seed, instructionArguments, prefix, idlTypes, generics)); + if (!results.every((r): r is NonNullable => r != null)) { + return undefined; + } + return { + definitions: results.map(r => r.definition), + values: results.flatMap(r => (r.value ? [r.value] : [])), + }; +} + +function resolveProgramSeed( + program: IdlV01Seed, + name: string, + instructionArguments: InstructionArgumentNode[], + prefix: string | undefined, + idlTypes: IdlV01TypeDef[], + generics: GenericsV01, +): { id?: string; value?: AccountValueNode | ArgumentValueNode } | undefined { + const result = pdaSeedNodeFromAnchorV01(program, instructionArguments, prefix, idlTypes, generics); + if (!result) { + logWarn(`Skipping PDA for account "${name}": program seed could not be resolved.`); + return undefined; + } + + if ( + isNode(result.definition, 'constantPdaSeedNode') && + isNode(result.definition.value, 'bytesValueNode') && + result.definition.value.encoding === 'base58' + ) { + return { id: result.definition.value.data }; + } + + if (result.value && isNode(result.value.value, ['accountValueNode', 'argumentValueNode'])) { + return { value: result.value.value }; + } + + return {}; +} diff --git a/packages/nodes-from-anchor/src/v01/InstructionNode.ts b/packages/nodes-from-anchor/src/v01/InstructionNode.ts index e93ce1db0..fae4c117a 100644 --- a/packages/nodes-from-anchor/src/v01/InstructionNode.ts +++ b/packages/nodes-from-anchor/src/v01/InstructionNode.ts @@ -16,8 +16,8 @@ import type { GenericsV01 } from './unwrapGenerics'; export function instructionNodeFromAnchorV01( idl: IdlV01Instruction, - generics: GenericsV01, idlTypes: IdlV01TypeDef[] = [], + generics: GenericsV01, ): InstructionNode { const name = idl.name; let dataArguments = idl.args.map(arg => instructionArgumentNodeFromAnchorV01(arg, generics)); diff --git a/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts b/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts index acaefcfe3..c42b98f08 100644 --- a/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts +++ b/packages/nodes-from-anchor/src/v01/PdaSeedNode.ts @@ -90,23 +90,16 @@ export function pdaSeedNodeFromAnchorV01( } // Anchor uses unprefixed strings for PDA seeds. - if ( + const isBorshString = isNode(argumentType, 'sizePrefixTypeNode') && isNode(argumentType.type, 'stringTypeNode') && argumentType.type.encoding === 'utf8' && isNode(argumentType.prefix, 'numberTypeNode') && - argumentType.prefix.format === 'u32' - ) { + argumentType.prefix.format === 'u32'; + if (isBorshString) { argumentType = stringTypeNode('utf8'); } - if (pathParts.length > 1) { - return { - definition: variablePdaSeedNode(argumentName, argumentType), - value: pdaSeedValueNode(argumentName, argumentValueNode(argumentName)), - }; - } - return { definition: variablePdaSeedNode(argumentName, argumentType), value: pdaSeedValueNode(argumentName, argumentValueNode(argumentName)), @@ -118,14 +111,14 @@ export function pdaSeedNodeFromAnchorV01( } function resolveNestedFieldType( - rootType: TypeNode, - fieldPath: string[], + parentType: TypeNode, + pathParts: string[], idlTypes: IdlV01TypeDef[], generics: GenericsV01, ): TypeNode | undefined { - let currentType = rootType; + let currentType = parentType; - for (const fieldName of fieldPath) { + for (const fieldName of pathParts) { const target = camelCase(fieldName); // Resolve type links before handling struct/tuple field lookup. diff --git a/packages/nodes-from-anchor/src/v01/ProgramNode.ts b/packages/nodes-from-anchor/src/v01/ProgramNode.ts index 5e1e52618..e132e5fa4 100644 --- a/packages/nodes-from-anchor/src/v01/ProgramNode.ts +++ b/packages/nodes-from-anchor/src/v01/ProgramNode.ts @@ -27,7 +27,7 @@ export function programNodeFromAnchorV01(idl: IdlV01): ProgramNode { definedTypes, errors: errors.map(errorNodeFromAnchorV01), events: events.map(event => eventNodeFromAnchorV01(event, types, generics)), - instructions: instructions.map(instruction => instructionNodeFromAnchorV01(instruction, generics, types)), + instructions: instructions.map(instruction => instructionNodeFromAnchorV01(instruction, types, generics)), name: idl.metadata.name, origin: 'anchor', publicKey: idl.address, diff --git a/packages/nodes-from-anchor/test/v01/InstructionNode.test.ts b/packages/nodes-from-anchor/test/v01/InstructionNode.test.ts index 7cd389fcb..682726e15 100644 --- a/packages/nodes-from-anchor/test/v01/InstructionNode.test.ts +++ b/packages/nodes-from-anchor/test/v01/InstructionNode.test.ts @@ -33,8 +33,8 @@ test('it creates instruction nodes', () => { discriminator: [246, 28, 6, 87, 251, 45, 50, 42], name: 'mintTokens', }, - generics, [{ name: 'Distribution', type: { fields: [{ name: 'group_mint', type: 'pubkey' }], kind: 'struct' } }], + generics, ); expect(node).toEqual( @@ -69,6 +69,7 @@ test('it creates instruction nodes with anchor discriminators', () => { discriminator: [246, 28, 6, 87, 251, 45, 50, 42], name: 'myInstruction', }, + [], generics, ); diff --git a/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts b/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts index 9c2543fee..2b360e7e6 100644 --- a/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts +++ b/packages/nodes-from-anchor/test/v01/pdaSeedNode.test.ts @@ -21,15 +21,15 @@ import { pdaSeedNodeFromAnchorV01 } from '../../src'; test('it creates a PdaSeedNode from a const Anchor seed', () => { const nodes = pdaSeedNodeFromAnchorV01({ kind: 'const', value: [11, 57, 246, 240] }, []); - expect(nodes!.definition).toEqual(constantPdaSeedNodeFromBytes('base58', 'HeLLo')); - expect(nodes!.value).toBeUndefined(); + expect(nodes?.definition).toEqual(constantPdaSeedNodeFromBytes('base58', 'HeLLo')); + expect(nodes?.value).toBeUndefined(); }); test('it creates a PdaSeedNode from an account Anchor seed', () => { const nodes = pdaSeedNodeFromAnchorV01({ kind: 'account', path: 'authority' }, []); - expect(nodes!.definition).toEqual(variablePdaSeedNode('authority', publicKeyTypeNode())); - expect(nodes!.value).toEqual(pdaSeedValueNode('authority', accountValueNode('authority'))); + expect(nodes?.definition).toEqual(variablePdaSeedNode('authority', publicKeyTypeNode())); + expect(nodes?.value).toEqual(pdaSeedValueNode('authority', accountValueNode('authority'))); }); test('it creates a PdaSeedNode from an arg Anchor seed', () => { @@ -37,8 +37,8 @@ test('it creates a PdaSeedNode from an arg Anchor seed', () => { instructionArgumentNode({ name: 'capacity', type: numberTypeNode('u64') }), ]); - expect(nodes!.definition).toEqual(variablePdaSeedNode('capacity', numberTypeNode('u64'))); - expect(nodes!.value).toEqual(pdaSeedValueNode('capacity', argumentValueNode('capacity'))); + expect(nodes?.definition).toEqual(variablePdaSeedNode('capacity', numberTypeNode('u64'))); + expect(nodes?.value).toEqual(pdaSeedValueNode('capacity', argumentValueNode('capacity'))); }); test('it resolves nested arg path from inline struct type', () => { @@ -52,8 +52,8 @@ test('it resolves nested arg path from inline struct type', () => { }), ]); - expect(nodes!.definition).toEqual(variablePdaSeedNode('owner', publicKeyTypeNode())); - expect(nodes!.value).toEqual(pdaSeedValueNode('owner', argumentValueNode('owner'))); + expect(nodes?.definition).toEqual(variablePdaSeedNode('owner', publicKeyTypeNode())); + expect(nodes?.value).toEqual(pdaSeedValueNode('owner', argumentValueNode('owner'))); }); test('it resolves nested arg path from defined type link', () => { @@ -64,8 +64,8 @@ test('it resolves nested arg path from defined type link', () => { [{ name: 'MyArgs', type: { fields: [{ name: 'amount', type: 'u64' }], kind: 'struct' } }], ); - expect(nodes!.definition).toEqual(variablePdaSeedNode('amount', numberTypeNode('u64'))); - expect(nodes!.value).toEqual(pdaSeedValueNode('amount', argumentValueNode('amount'))); + expect(nodes?.definition).toEqual(variablePdaSeedNode('amount', numberTypeNode('u64'))); + expect(nodes?.value).toEqual(pdaSeedValueNode('amount', argumentValueNode('amount'))); }); test('it uses full nested path to avoid name collisions for deeply nested arg seeds', () => { @@ -109,8 +109,8 @@ test('it resolves nested account path from type def', () => { [{ name: 'Mint', type: { fields: [{ name: 'authority', type: 'pubkey' }], kind: 'struct' } }], ); - expect(nodes!.definition).toEqual(variablePdaSeedNode('mintAuthority', publicKeyTypeNode())); - expect(nodes!.value).toEqual(pdaSeedValueNode('mintAuthority', accountValueNode('mint'))); + expect(nodes?.definition).toEqual(variablePdaSeedNode('mintAuthority', publicKeyTypeNode())); + expect(nodes?.value).toEqual(pdaSeedValueNode('mintAuthority', accountValueNode('mint'))); }); test('it returns undefined for unresolvable nested account path', () => { @@ -176,6 +176,6 @@ test('it removes the string prefix from arg Anchor seeds', () => { }), ]); - expect(nodes!.definition).toEqual(variablePdaSeedNode('identifier', stringTypeNode('utf8'))); - expect(nodes!.value).toEqual(pdaSeedValueNode('identifier', argumentValueNode('identifier'))); + expect(nodes?.definition).toEqual(variablePdaSeedNode('identifier', stringTypeNode('utf8'))); + expect(nodes?.value).toEqual(pdaSeedValueNode('identifier', argumentValueNode('identifier'))); });