Kaynağa Gözat

Add programId to PdaValueNode (#915)

Loris Leiva 2 gün önce
ebeveyn
işleme
5c3fb46cf5

+ 7 - 0
.changeset/bumpy-ideas-share.md

@@ -0,0 +1,7 @@
+---
+'@codama/node-from-anchor': minor
+'@codama/node-types': minor
+'@codama/nodes': minor
+---
+
+Add optional programId field to PdaValueNode to allow using a provided account as the program of the PDA

+ 10 - 1
packages/node-types/src/contextualValueNodes/PdaValueNode.ts

@@ -1,11 +1,20 @@
 import type { PdaLinkNode } from '../linkNodes';
 import type { PdaNode } from '../PdaNode';
+import type { AccountValueNode } from './AccountValueNode';
+import type { ArgumentValueNode } from './ArgumentValueNode';
 import type { PdaSeedValueNode } from './PdaSeedValueNode';
 
-export interface PdaValueNode<TSeeds extends PdaSeedValueNode[] = PdaSeedValueNode[]> {
+export interface PdaValueNode<
+    TSeeds extends PdaSeedValueNode[] = PdaSeedValueNode[],
+    TProgram extends AccountValueNode | ArgumentValueNode | undefined =
+        | AccountValueNode
+        | ArgumentValueNode
+        | undefined,
+> {
     readonly kind: 'pdaValueNode';
 
     // Children.
     readonly pda: PdaLinkNode | PdaNode;
     readonly seeds: TSeeds;
+    readonly programId?: TProgram;
 }

+ 24 - 103
packages/nodes-from-anchor/src/v01/InstructionAccountNode.ts

@@ -1,54 +1,36 @@
 import {
-    CODAMA_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING,
-    CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING,
-    CODAMA_ERROR__ANCHOR__PROGRAM_ID_KIND_UNIMPLEMENTED,
-    CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED,
-    CODAMA_ERROR__ANCHOR__TYPE_PATH_MISSING,
-    CodamaError,
-} from '@codama/errors';
-import {
-    AccountNode,
-    accountValueNode,
-    argumentValueNode,
-    camelCase,
-    constantPdaSeedNodeFromBytes,
+    AccountValueNode,
+    ArgumentValueNode,
     InstructionAccountNode,
     instructionAccountNode,
     InstructionArgumentNode,
+    isNode,
     pdaNode,
     PdaSeedNode,
     PdaSeedValueNode,
-    pdaSeedValueNode,
     PdaValueNode,
     pdaValueNode,
-    publicKeyTypeNode,
     PublicKeyValueNode,
     publicKeyValueNode,
-    resolveNestedTypeNode,
-    variablePdaSeedNode,
 } from '@codama/nodes';
-import { getBase58Codec } from '@solana/codecs';
 
-import { hex } from '../utils';
 import { IdlV01InstructionAccount, IdlV01InstructionAccountItem, IdlV01Seed } from './idl';
+import { pdaSeedNodeFromAnchorV01 } from './PdaSeedNode';
 
 export function instructionAccountNodesFromAnchorV01(
-    allAccounts: AccountNode[],
-    instructionArguments: InstructionArgumentNode[],
     idl: IdlV01InstructionAccountItem[],
+    instructionArguments: InstructionArgumentNode[],
 ): InstructionAccountNode[] {
     return idl.flatMap(account =>
         'accounts' in account
-            ? instructionAccountNodesFromAnchorV01(allAccounts, instructionArguments, account.accounts)
-            : [instructionAccountNodeFromAnchorV01(allAccounts, instructionArguments, account, idl)],
+            ? instructionAccountNodesFromAnchorV01(account.accounts, instructionArguments)
+            : [instructionAccountNodeFromAnchorV01(account, instructionArguments)],
     );
 }
 
 export function instructionAccountNodeFromAnchorV01(
-    allAccounts: AccountNode[],
-    instructionArguments: InstructionArgumentNode[],
     idl: IdlV01InstructionAccount,
-    parentIdl: IdlV01InstructionAccountItem[],
+    instructionArguments: InstructionArgumentNode[],
 ): InstructionAccountNode {
     const isOptional = idl.optional ?? false;
     const docs = idl.docs ?? [];
@@ -64,94 +46,33 @@ export function instructionAccountNodeFromAnchorV01(
         // 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 [seeds, lookups] = idl.pda.seeds.reduce(
+            const [seedDefinitions, seedValues] = idl.pda.seeds.reduce(
                 ([seeds, lookups], seed: IdlV01Seed) => {
-                    const kind = seed.kind;
-
-                    switch (kind) {
-                        case 'const':
-                            return [[...seeds, constantPdaSeedNodeFromBytes('base16', hex(seed.value))], lookups];
-                        case 'account': {
-                            const path = seed.path.split('.');
-                            if (path.length === 1) {
-                                return [
-                                    [...seeds, variablePdaSeedNode(seed.path, publicKeyTypeNode())],
-                                    [...lookups, pdaSeedValueNode(seed.path, accountValueNode(seed.path))],
-                                ];
-                            } else if (path.length === 2) {
-                                // TODO: Handle nested account paths.
-                                // Currently, this scenario is never reached.
-
-                                const accountName = camelCase(seed.account ?? '');
-                                const accountNode = allAccounts.find(({ name }) => name === accountName);
-                                if (!accountNode) {
-                                    throw new CodamaError(CODAMA_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING, { kind });
-                                }
-
-                                const fieldName = camelCase(path[1]);
-                                const accountFields = resolveNestedTypeNode(accountNode.data).fields;
-                                const fieldNode = accountFields.find(({ name }) => name === fieldName);
-                                if (!fieldNode) {
-                                    throw new CodamaError(CODAMA_ERROR__ANCHOR__TYPE_PATH_MISSING, {
-                                        idlType: seed.account,
-                                        path: seed.path,
-                                    });
-                                }
-
-                                const seedName = camelCase(seed.path);
-                                return [[...seeds, variablePdaSeedNode(seedName, fieldNode.type)], []];
-                            } else {
-                                throw new CodamaError(CODAMA_ERROR__ANCHOR__TYPE_PATH_MISSING, {
-                                    idlType: seed,
-                                    path: seed.path,
-                                });
-                            }
-                        }
-                        case 'arg': {
-                            const argumentName = camelCase(seed.path);
-                            const argumentNode = instructionArguments.find(({ name }) => name === argumentName);
-                            if (!argumentNode) {
-                                throw new CodamaError(CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING, { name: seed.path });
-                            }
-
-                            return [
-                                [...seeds, variablePdaSeedNode(seed.path, argumentNode.type)],
-                                [...lookups, pdaSeedValueNode(seed.path, argumentValueNode(seed.path))],
-                            ];
-                        }
-                        default:
-                            throw new CodamaError(CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED, { kind });
-                    }
+                    const { definition, value } = pdaSeedNodeFromAnchorV01(seed, instructionArguments);
+                    return [[...seeds, definition], value ? [...lookups, value] : lookups];
                 },
                 <[PdaSeedNode[], PdaSeedValueNode[]]>[[], []],
             );
 
             let programId: string | undefined;
+            let programIdValue: AccountValueNode | ArgumentValueNode | undefined;
             if (idl.pda.program !== undefined) {
-                const kind = idl.pda.program.kind;
-                switch (kind) {
-                    case 'const': {
-                        programId = getBase58Codec().decode(new Uint8Array(idl.pda.program.value));
-                        break;
-                    }
-                    case 'account': {
-                        const programPath = idl.pda.program.path;
-                        const programNode = parentIdl.find(acc => acc.name == programPath);
-                        if (!(programNode && 'address' in programNode)) {
-                            throw new CodamaError(CODAMA_ERROR__ANCHOR__PROGRAM_ID_KIND_UNIMPLEMENTED, { kind });
-                        }
-                        programId = programNode.address;
-                        break;
-                    }
-                    default: {
-                        throw new CodamaError(CODAMA_ERROR__ANCHOR__PROGRAM_ID_KIND_UNIMPLEMENTED, { kind });
-                    }
+                const { definition, value } = pdaSeedNodeFromAnchorV01(idl.pda.program, instructionArguments);
+                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;
                 }
             }
 
             defaultValue = pdaValueNode(
-                pdaNode({ name, seeds, ...(programId !== undefined ? { programId } : {}) }),
-                lookups,
+                pdaNode({ name, programId, seeds: seedDefinitions }),
+                seedValues,
+                programIdValue,
             );
         }
     }

+ 2 - 7
packages/nodes-from-anchor/src/v01/InstructionNode.ts

@@ -1,5 +1,4 @@
 import {
-    AccountNode,
     bytesTypeNode,
     camelCase,
     fieldDiscriminatorNode,
@@ -15,11 +14,7 @@ import { instructionAccountNodesFromAnchorV01 } from './InstructionAccountNode';
 import { instructionArgumentNodeFromAnchorV01 } from './InstructionArgumentNode';
 import type { GenericsV01 } from './unwrapGenerics';
 
-export function instructionNodeFromAnchorV01(
-    allAccounts: AccountNode[],
-    idl: IdlV01Instruction,
-    generics: GenericsV01,
-): InstructionNode {
+export function instructionNodeFromAnchorV01(idl: IdlV01Instruction, generics: GenericsV01): InstructionNode {
     const name = idl.name;
     let dataArguments = idl.args.map(arg => instructionArgumentNodeFromAnchorV01(arg, generics));
 
@@ -33,7 +28,7 @@ export function instructionNodeFromAnchorV01(
     const discriminators = [fieldDiscriminatorNode('discriminator')];
 
     return instructionNode({
-        accounts: instructionAccountNodesFromAnchorV01(allAccounts, dataArguments, idl.accounts ?? []),
+        accounts: instructionAccountNodesFromAnchorV01(idl.accounts ?? [], dataArguments),
         arguments: dataArguments,
         discriminators,
         docs: idl.docs ?? [],

+ 55 - 0
packages/nodes-from-anchor/src/v01/PdaSeedNode.ts

@@ -0,0 +1,55 @@
+import {
+    CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING,
+    CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED,
+    CodamaError,
+} from '@codama/errors';
+import {
+    accountValueNode,
+    argumentValueNode,
+    constantPdaSeedNodeFromBytes,
+    InstructionArgumentNode,
+    PdaSeedNode,
+    PdaSeedValueNode,
+    pdaSeedValueNode,
+    publicKeyTypeNode,
+    variablePdaSeedNode,
+} from '@codama/nodes';
+import { getBase58Codec } from '@solana/codecs';
+
+import { IdlV01Seed } from './idl';
+
+export function pdaSeedNodeFromAnchorV01(
+    seed: IdlV01Seed,
+    instructionArguments: InstructionArgumentNode[],
+): Readonly<{ definition: PdaSeedNode; value?: PdaSeedValueNode }> {
+    const kind = seed.kind;
+
+    switch (kind) {
+        case 'const':
+            return {
+                definition: constantPdaSeedNodeFromBytes('base58', getBase58Codec().decode(new Uint8Array(seed.value))),
+            };
+        case 'account': {
+            // Ignore nested paths.
+            const [accountName] = seed.path.split('.');
+            return {
+                definition: variablePdaSeedNode(accountName, publicKeyTypeNode()),
+                value: pdaSeedValueNode(accountName, accountValueNode(accountName)),
+            };
+        }
+        case 'arg': {
+            // Ignore nested paths.
+            const [argumentName] = seed.path.split('.');
+            const argumentNode = instructionArguments.find(({ name }) => name === argumentName);
+            if (!argumentNode) {
+                throw new CodamaError(CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING, { name: argumentName });
+            }
+            return {
+                definition: variablePdaSeedNode(argumentName, argumentNode.type),
+                value: pdaSeedValueNode(argumentName, argumentValueNode(argumentName)),
+            };
+        }
+        default:
+            throw new CodamaError(CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED, { kind });
+    }
+}

+ 1 - 3
packages/nodes-from-anchor/src/v01/ProgramNode.ts

@@ -21,9 +21,7 @@ export function programNodeFromAnchorV01(idl: IdlV01): ProgramNode {
         accounts: accountNodes,
         definedTypes,
         errors: errors.map(errorNodeFromAnchorV01),
-        instructions: instructions.map(instruction =>
-            instructionNodeFromAnchorV01(accountNodes, instruction, generics),
-        ),
+        instructions: instructions.map(instruction => instructionNodeFromAnchorV01(instruction, generics)),
         name: idl.metadata.name,
         origin: 'anchor',
         publicKey: idl.address,

+ 1 - 0
packages/nodes-from-anchor/src/v01/index.ts

@@ -5,6 +5,7 @@ export * from './idl';
 export * from './InstructionAccountNode';
 export * from './InstructionArgumentNode';
 export * from './InstructionNode';
+export * from './PdaSeedNode';
 export * from './ProgramNode';
 export * from './RootNode';
 export * from './typeNodes';

+ 53 - 40
packages/nodes-from-anchor/test/v01/InstructionAccountNode.test.ts

@@ -1,5 +1,4 @@
 import {
-    accountNode,
     accountValueNode,
     argumentValueNode,
     constantPdaSeedNodeFromBytes,
@@ -11,9 +10,6 @@ import {
     pdaValueNode,
     publicKeyTypeNode,
     publicKeyValueNode,
-    sizePrefixTypeNode,
-    structFieldTypeNode,
-    structTypeNode,
     variablePdaSeedNode,
 } from '@codama/nodes';
 import { expect, test } from 'vitest';
@@ -22,8 +18,6 @@ import { instructionAccountNodeFromAnchorV01, instructionAccountNodesFromAnchorV
 
 test('it creates instruction account nodes', () => {
     const node = instructionAccountNodeFromAnchorV01(
-        [],
-        [],
         {
             docs: ['my docs'],
             name: 'MyInstructionAccount',
@@ -47,13 +41,6 @@ test('it creates instruction account nodes', () => {
 
 test('it flattens nested instruction accounts', () => {
     const nodes = instructionAccountNodesFromAnchorV01(
-        [],
-        [
-            instructionArgumentNode({
-                name: 'amount',
-                type: numberTypeNode('u8'),
-            }),
-        ],
         [
             { name: 'accountA', signer: false, writable: false },
             {
@@ -93,6 +80,7 @@ test('it flattens nested instruction accounts', () => {
             },
             { name: 'account_d', signer: true, writable: true },
         ],
+        [instructionArgumentNode({ name: 'amount', type: numberTypeNode('u8') })],
     );
 
     expect(nodes).toEqual([
@@ -103,7 +91,7 @@ test('it flattens nested instruction accounts', () => {
                 pdaNode({
                     name: 'accountC',
                     seeds: [
-                        constantPdaSeedNodeFromBytes('base16', '00010203'),
+                        constantPdaSeedNodeFromBytes('base58', '1Ldp'),
                         variablePdaSeedNode('accountB', publicKeyTypeNode()),
                         variablePdaSeedNode('amount', numberTypeNode('u8')),
                     ],
@@ -129,16 +117,15 @@ test('it flattens nested instruction accounts', () => {
 
 test('it ignores PDA default values if at least one seed as a path of length greater than 1', () => {
     const nodes = instructionAccountNodesFromAnchorV01(
-        [
-            accountNode({
-                data: sizePrefixTypeNode(
-                    structTypeNode([structFieldTypeNode({ name: 'authority', type: publicKeyTypeNode() })]),
-                    numberTypeNode('u32'),
-                ),
-                name: 'mint',
-            }),
-        ],
-        [],
+        // [
+        //     accountNode({
+        //         data: sizePrefixTypeNode(
+        //             structTypeNode([structFieldTypeNode({ name: 'authority', type: publicKeyTypeNode() })]),
+        //             numberTypeNode('u32'),
+        //         ),
+        //         name: 'mint',
+        //     }),
+        // ],
         [
             {
                 name: 'somePdaAccount',
@@ -155,6 +142,7 @@ test('it ignores PDA default values if at least one seed as a path of length gre
                 writable: false,
             },
         ],
+        [],
     );
 
     expect(nodes).toEqual([
@@ -168,8 +156,6 @@ test('it ignores PDA default values if at least one seed as a path of length gre
 
 test('it handles PDAs with a constant program id', () => {
     const nodes = instructionAccountNodesFromAnchorV01(
-        [],
-        [],
         [
             {
                 name: 'program_data',
@@ -193,6 +179,7 @@ test('it handles PDAs with a constant program id', () => {
                 },
             },
         ],
+        [],
     );
 
     expect(nodes).toEqual([
@@ -201,12 +188,7 @@ test('it handles PDAs with a constant program id', () => {
                 pdaNode({
                     name: 'programData',
                     programId: 'BPFLoaderUpgradeab1e11111111111111111111111',
-                    seeds: [
-                        constantPdaSeedNodeFromBytes(
-                            'base16',
-                            'a6af97eea643579472d10d58bae4cec5b64781c3ceece5dfb83c61f93f5ccb1b',
-                        ),
-                    ],
+                    seeds: [constantPdaSeedNodeFromBytes('base58', 'CDfyUBS8ZuL1L3kEy6mHVyAx1s9E97KNAwTfMfvhCriN')],
                 }),
                 [],
             ),
@@ -217,18 +199,48 @@ test('it handles PDAs with a constant program id', () => {
     ]);
 });
 
-test.skip('it handles account data paths of length 2', () => {
+test('it handles PDAs with a program id that points to another account', () => {
     const nodes = instructionAccountNodesFromAnchorV01(
         [
-            accountNode({
-                data: sizePrefixTypeNode(
-                    structTypeNode([structFieldTypeNode({ name: 'authority', type: publicKeyTypeNode() })]),
-                    numberTypeNode('u32'),
-                ),
-                name: 'mint',
-            }),
+            {
+                name: 'my_pda',
+                pda: {
+                    program: { kind: 'account', path: 'my_program' },
+                    seeds: [],
+                },
+            },
         ],
         [],
+    );
+
+    expect(nodes).toEqual([
+        instructionAccountNode({
+            defaultValue: pdaValueNode(
+                pdaNode({
+                    name: 'myPda',
+                    seeds: [],
+                }),
+                [],
+                accountValueNode('myProgram'),
+            ),
+            isSigner: false,
+            isWritable: false,
+            name: 'myPda',
+        }),
+    ]);
+});
+
+test.skip('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',
@@ -245,6 +257,7 @@ test.skip('it handles account data paths of length 2', () => {
                 writable: false,
             },
         ],
+        [],
     );
 
     expect(nodes).toEqual([

+ 3 - 23
packages/nodes-from-anchor/test/v01/InstructionNode.test.ts

@@ -1,5 +1,4 @@
 import {
-    accountNode,
     bytesTypeNode,
     fieldDiscriminatorNode,
     fixedSizeTypeNode,
@@ -7,9 +6,6 @@ import {
     instructionArgumentNode,
     instructionNode,
     numberTypeNode,
-    publicKeyTypeNode,
-    structFieldTypeNode,
-    structTypeNode,
 } from '@codama/nodes';
 import { expect, test } from 'vitest';
 
@@ -19,21 +15,6 @@ const generics = {} as GenericsV01;
 
 test('it creates instruction nodes', () => {
     const node = instructionNodeFromAnchorV01(
-        [
-            accountNode({
-                data: structTypeNode([
-                    structFieldTypeNode({
-                        name: 'groupMint',
-                        type: publicKeyTypeNode(),
-                    }),
-                    structFieldTypeNode({
-                        name: 'paymentMint',
-                        type: publicKeyTypeNode(),
-                    }),
-                ]),
-                name: 'distribution',
-            }),
-        ],
         {
             accounts: [
                 {
@@ -59,16 +40,16 @@ test('it creates instruction nodes', () => {
         instructionNode({
             accounts: [
                 instructionAccountNode({
-                    // TODO: Handle seeds with nested paths.
+                    // TODO: Handle seeds with nested paths. (Needs a path in the IDL but should we?)
                     // defaultValue: pdaValueNode(
                     //     pdaNode({
                     //         name: 'distribution',
                     //         seeds: [
-                    //             constantPdaSeedNodeFromBytes('base16', '2a1f1d'),
+                    //             constantPdaSeedNodeFromBytes('base58', 'F9bS'),
                     //             variablePdaSeedNode('distributionGroupMint', publicKeyTypeNode()),
                     //         ],
                     //     }),
-                    //     [],
+                    //     [pdaSeedValueNode("distributionGroupMint", accountValueNode('distribution', 'group_mint'))],
                     // ),
                     isSigner: false,
                     isWritable: true,
@@ -92,7 +73,6 @@ test('it creates instruction nodes', () => {
 
 test('it creates instruction nodes with anchor discriminators', () => {
     const node = instructionNodeFromAnchorV01(
-        [],
         {
             accounts: [],
             args: [],

+ 1 - 1
packages/nodes-from-anchor/test/v01/ProgramNode.test.ts

@@ -108,7 +108,7 @@ test('it creates program nodes', () => {
                                 pdaNode({
                                     name: 'authority',
                                     seeds: [
-                                        constantPdaSeedNodeFromBytes('base16', '2a1f1d'),
+                                        constantPdaSeedNodeFromBytes('base58', 'F9bS'),
                                         variablePdaSeedNode('owner', publicKeyTypeNode()),
                                         variablePdaSeedNode('amount', numberTypeNode('u8')),
                                     ],

+ 15 - 3
packages/nodes/src/contextualValueNodes/PdaValueNode.ts

@@ -1,16 +1,28 @@
-import type { PdaLinkNode, PdaNode, PdaSeedValueNode, PdaValueNode } from '@codama/node-types';
+import type {
+    AccountValueNode,
+    ArgumentValueNode,
+    PdaLinkNode,
+    PdaNode,
+    PdaSeedValueNode,
+    PdaValueNode,
+} from '@codama/node-types';
 
 import { pdaLinkNode } from '../linkNodes';
 
-export function pdaValueNode<const TSeeds extends PdaSeedValueNode[] = []>(
+export function pdaValueNode<
+    const TSeeds extends PdaSeedValueNode[] = [],
+    const TProgram extends AccountValueNode | ArgumentValueNode | undefined = undefined,
+>(
     pda: PdaLinkNode | PdaNode | string,
     seeds: TSeeds = [] as PdaSeedValueNode[] as TSeeds,
-): PdaValueNode<TSeeds> {
+    programId: TProgram = undefined as TProgram,
+): PdaValueNode<TSeeds, TProgram> {
     return Object.freeze({
         kind: 'pdaValueNode',
 
         // Children.
         pda: typeof pda === 'string' ? pdaLinkNode(pda) : pda,
         seeds,
+        ...(programId ? { programId } : {}),
     });
 }