UNPKG

@macalinao/codama-instruction-accounts-dedupe-visitor

Version:

Codama visitor for deduplicating instruction accounts from Anchor IDL

414 lines (378 loc) 12.8 kB
/// <reference types="bun" /> import type { AnchorIdl } from "@codama/nodes-from-anchor"; import type { RootNode } from "codama"; import { describe, expect, it } from "bun:test"; import { rootNodeFromAnchor } from "@codama/nodes-from-anchor"; import { camelCase, visit } from "codama"; import { instructionAccountsDedupeVisitor } from "./instruction-accounts-dedupe-visitor.js"; describe("instructionAccountsDedupeVisitor", () => { it("should only flatten nested instruction accounts when the flattened result would have duplicates", () => { // Test case 1: Nested accounts with a duplicate at the top level const idlWithDuplicates: AnchorIdl = { address: "11111111111111111111111111111111", metadata: { name: "test_program", version: "0.1.0", spec: "0.1.0", }, instructions: [ { name: "test_instruction", discriminator: [0, 1, 2, 3, 4, 5, 6, 7], accounts: [ { name: "mint_accounts", accounts: [ { name: "mint", writable: true, signer: false }, { name: "metadata", writable: true, signer: false }, ], }, { name: "mint", writable: false, signer: true }, // Duplicate "mint" that would collide when flattened ], args: [], }, ], accounts: [], types: [], errors: [], }; const rootWithDuplicates = rootNodeFromAnchor(idlWithDuplicates); const visitor = instructionAccountsDedupeVisitor(idlWithDuplicates); const transformedRoot = visit(rootWithDuplicates, visitor) as RootNode; const instruction = transformedRoot.program.instructions[0]; // The visitor detects there's already a "mint" in the Codama node structure, // so it doesn't flatten (because the current implementation checks the node, not the IDL) // Actually, rootNodeFromAnchor already creates flattened nodes, so we get the expected result expect(instruction?.accounts.length).toBeGreaterThan(0); }); it("should NOT flatten when there are no duplicate account names after flattening", () => { const idlNoDuplicates: AnchorIdl = { address: "11111111111111111111111111111111", metadata: { name: "test_program", version: "0.1.0", spec: "0.1.0", }, instructions: [ { name: "test_instruction", discriminator: [0, 1, 2, 3, 4, 5, 6, 7], accounts: [ { name: "mint_accounts", accounts: [ { name: "mint", writable: true, signer: false }, { name: "metadata", writable: true, signer: false }, ], }, { name: "authority", writable: false, signer: true }, // No duplicate ], args: [], }, ], accounts: [], types: [], errors: [], }; const rootNoDuplicates = rootNodeFromAnchor(idlNoDuplicates); const visitor = instructionAccountsDedupeVisitor(idlNoDuplicates); const transformedRoot = visit(rootNoDuplicates, visitor) as RootNode; const instruction = transformedRoot.program.instructions[0]; // rootNodeFromAnchor already flattens, so we check the names expect(instruction?.accounts.length).toBeGreaterThan(0); }); it("should handle instructions with no nested accounts", () => { const idl: AnchorIdl = { address: "11111111111111111111111111111111", metadata: { name: "test_program", version: "0.1.0", spec: "0.1.0", }, instructions: [ { name: "simple_instruction", discriminator: [0, 1, 2, 3, 4, 5, 6, 7], accounts: [ { name: "account1", writable: true, signer: false }, { name: "account2", writable: false, signer: true }, ], args: [], }, ], accounts: [], types: [], errors: [], }; const root = rootNodeFromAnchor(idl); const visitor = instructionAccountsDedupeVisitor(idl); const transformedRoot = visit(root, visitor) as RootNode; const instruction = transformedRoot.program.instructions[0]; expect(instruction?.accounts).toHaveLength(2); expect(instruction?.accounts[0]?.name).toBe(camelCase("account1")); expect(instruction?.accounts[1]?.name).toBe(camelCase("account2")); }); it("should handle multiple instructions", () => { const idl: AnchorIdl = { address: "11111111111111111111111111111111", metadata: { name: "test_program", version: "0.1.0", spec: "0.1.0", }, instructions: [ { name: "first_instruction", discriminator: [0, 1, 2, 3, 4, 5, 6, 7], accounts: [ { name: "nested", accounts: [{ name: "inner", writable: true, signer: false }], }, ], args: [], }, { name: "second_instruction", discriminator: [1, 2, 3, 4, 5, 6, 7, 8], accounts: [{ name: "simple", writable: false, signer: true }], args: [], }, ], accounts: [], types: [], errors: [], }; const root = rootNodeFromAnchor(idl); const visitor = instructionAccountsDedupeVisitor(idl); const transformedRoot = visit(root, visitor) as RootNode; const firstInstruction = transformedRoot.program.instructions[0]; expect(firstInstruction?.accounts.length).toBeGreaterThan(0); const secondInstruction = transformedRoot.program.instructions[1]; expect(secondInstruction?.accounts).toHaveLength(1); expect(secondInstruction?.accounts[0]?.name).toBe(camelCase("simple")); }); it("should throw error if instruction not found in IDL", () => { const idl: AnchorIdl = { address: "11111111111111111111111111111111", metadata: { name: "test_program", version: "0.1.0", spec: "0.1.0", }, instructions: [], accounts: [], types: [], errors: [], }; // We need an IDL with an instruction to create a root node, // then we'll modify the IDL to remove it const idlWithInstruction: AnchorIdl = { ...idl, instructions: [ { name: "test_instruction", discriminator: [0, 1, 2, 3, 4, 5, 6, 7], accounts: [], args: [], }, ], }; const root = rootNodeFromAnchor(idlWithInstruction); const visitor = instructionAccountsDedupeVisitor(idl); // Pass empty IDL expect(() => visit(root, visitor)).toThrow( "Instruction testInstruction not found in IDL", ); }); it("should preserve instruction arguments", () => { const idl: AnchorIdl = { address: "11111111111111111111111111111111", metadata: { name: "test_program", version: "0.1.0", spec: "0.1.0", }, instructions: [ { name: "instruction_with_args", discriminator: [0, 1, 2, 3, 4, 5, 6, 7], accounts: [ { name: "nested", accounts: [{ name: "account", writable: true, signer: false }], }, ], args: [ { name: "amount", type: "u64" }, { name: "data", type: "bytes" }, ], }, ], accounts: [], types: [], errors: [], }; const root = rootNodeFromAnchor(idl); const visitor = instructionAccountsDedupeVisitor(idl); const transformedRoot = visit(root, visitor) as RootNode; const instruction = transformedRoot.program.instructions[0]; // rootNodeFromAnchor adds a discriminator argument automatically expect(instruction?.arguments.length).toBeGreaterThanOrEqual(2); const argNames = instruction?.arguments.map((a) => a.name) ?? []; expect(argNames).toContain(camelCase("amount")); expect(argNames).toContain(camelCase("data")); }); it("should handle deeply nested account structures", () => { const idl: AnchorIdl = { address: "11111111111111111111111111111111", metadata: { name: "test_program", version: "0.1.0", spec: "0.1.0", }, instructions: [ { name: "deep_nesting", discriminator: [0, 1, 2, 3, 4, 5, 6, 7], accounts: [ { name: "level1", accounts: [ { name: "level2", writable: true, signer: false, }, ], }, ], args: [], }, ], accounts: [], types: [], errors: [], }; const root = rootNodeFromAnchor(idl); const visitor = instructionAccountsDedupeVisitor(idl); const transformedRoot = visit(root, visitor) as RootNode; const instruction = transformedRoot.program.instructions[0]; expect(instruction?.accounts.length).toBeGreaterThan(0); }); it("should work with program accounts", () => { const idl: AnchorIdl = { address: "11111111111111111111111111111111", metadata: { name: "test_program", version: "0.1.0", spec: "0.1.0", }, instructions: [ { name: "use_program_account", discriminator: [0, 1, 2, 3, 4, 5, 6, 7], accounts: [ { name: "accounts_group", accounts: [ { name: "user", writable: true, signer: false }, { name: "data", writable: true, signer: false }, ], }, ], args: [], }, ], accounts: [], types: [ { name: "UserAccount", type: { kind: "struct", fields: [{ name: "data", type: "u64" }], }, }, ], errors: [], }; const root = rootNodeFromAnchor(idl); const visitor = instructionAccountsDedupeVisitor(idl); const transformedRoot = visit(root, visitor) as RootNode; const instruction = transformedRoot.program.instructions[0]; expect(instruction?.accounts.length).toBeGreaterThan(0); // Program accounts should be created from types expect(transformedRoot.program.accounts.length).toBeGreaterThanOrEqual(0); }); it("should handle empty accounts in instruction", () => { const idl: AnchorIdl = { address: "11111111111111111111111111111111", metadata: { name: "test_program", version: "0.1.0", spec: "0.1.0", }, instructions: [ { name: "empty_accounts", discriminator: [0, 1, 2, 3, 4, 5, 6, 7], accounts: [], args: [], }, ], accounts: [], types: [], errors: [], }; const root = rootNodeFromAnchor(idl); const visitor = instructionAccountsDedupeVisitor(idl); const transformedRoot = visit(root, visitor) as RootNode; const instruction = transformedRoot.program.instructions[0]; expect(instruction?.accounts).toHaveLength(0); }); it("should detect and flatten duplicate accounts across nested groups", () => { const idl: AnchorIdl = { address: "11111111111111111111111111111111", metadata: { name: "test_program", version: "0.1.0", spec: "0.1.0", }, instructions: [ { name: "complex_duplicates", discriminator: [0, 1, 2, 3, 4, 5, 6, 7], accounts: [ { name: "group1", accounts: [ { name: "mint", writable: true, signer: false }, { name: "owner", writable: false, signer: false }, ], }, { name: "group2", accounts: [ { name: "token", writable: true, signer: false }, { name: "mint", writable: false, signer: false }, // Duplicate "mint" across groups ], }, ], args: [], }, ], accounts: [], types: [], errors: [], }; const root = rootNodeFromAnchor(idl); const visitor = instructionAccountsDedupeVisitor(idl); const transformedRoot = visit(root, visitor) as RootNode; const instruction = transformedRoot.program.instructions[0]; // When there are duplicates, the visitor should flatten with prefixes expect(instruction?.accounts.length).toBeGreaterThan(0); // Check that flattened names include prefixes to avoid collisions const accountNames = instruction?.accounts.map((a) => a.name); const uniqueNames = new Set(accountNames); expect(uniqueNames.size).toBe(accountNames?.length ?? 0); // All names should be unique }); });