Loris Leiva 1 рік тому
батько
коміт
d86d7feb96
47 змінених файлів з 3810 додано та 1 видалено
  1. 1 1
      packages/visitors-core/package.json
  2. 332 0
      packages/visitors-core/test/NodeSelector.test.ts
  3. 1 0
      packages/visitors/.gitignore
  4. 1 0
      packages/visitors/.prettierignore
  5. 23 0
      packages/visitors/LICENSE
  6. 3 0
      packages/visitors/README.md
  7. 68 0
      packages/visitors/package.json
  8. 30 0
      packages/visitors/src/addPdasVisitor.ts
  9. 99 0
      packages/visitors/src/createSubInstructionsFromEnumArgsVisitor.ts
  10. 67 0
      packages/visitors/src/deduplicateIdenticalDefinedTypesVisitor.ts
  11. 39 0
      packages/visitors/src/defaultVisitor.ts
  12. 98 0
      packages/visitors/src/fillDefaultPdaSeedValuesVisitor.ts
  13. 58 0
      packages/visitors/src/flattenInstructionDataArgumentsVisitor.ts
  14. 53 0
      packages/visitors/src/flattenStructVisitor.ts
  15. 118 0
      packages/visitors/src/getByteSizeVisitor.ts
  16. 96 0
      packages/visitors/src/getDefinedTypeHistogramVisitor.ts
  17. 245 0
      packages/visitors/src/getResolvedInstructionInputsVisitor.ts
  18. 29 0
      packages/visitors/src/index.ts
  19. 35 0
      packages/visitors/src/renameHelpers.ts
  20. 49 0
      packages/visitors/src/setAccountDiscriminatorFromFieldVisitor.ts
  21. 26 0
      packages/visitors/src/setFixedAccountSizesVisitor.ts
  22. 196 0
      packages/visitors/src/setInstructionAccountDefaultValuesVisitor.ts
  23. 51 0
      packages/visitors/src/setInstructionDiscriminatorsVisitor.ts
  24. 32 0
      packages/visitors/src/setNumberWrappersVisitor.ts
  25. 80 0
      packages/visitors/src/setStructDefaultValuesVisitor.ts
  26. 30 0
      packages/visitors/src/transformDefinedTypesIntoAccountsVisitor.ts
  27. 37 0
      packages/visitors/src/transformU8ArraysToBytesVisitor.ts
  28. 55 0
      packages/visitors/src/unwrapDefinedTypesVisitor.ts
  29. 35 0
      packages/visitors/src/unwrapInstructionArgsDefinedTypesVisitor.ts
  30. 78 0
      packages/visitors/src/unwrapTupleEnumWithSingleStructVisitor.ts
  31. 30 0
      packages/visitors/src/unwrapTypeDefinedLinksVisitor.ts
  32. 123 0
      packages/visitors/src/updateAccountsVisitor.ts
  33. 64 0
      packages/visitors/src/updateDefinedTypesVisitor.ts
  34. 19 0
      packages/visitors/src/updateErrorsVisitor.ts
  35. 170 0
      packages/visitors/src/updateInstructionsVisitor.ts
  36. 39 0
      packages/visitors/src/updateProgramsVisitor.ts
  37. 96 0
      packages/visitors/test/addPdasVisitor.test.ts
  38. 152 0
      packages/visitors/test/fillDefaultPdaSeedValuesVisitor.test.ts
  39. 93 0
      packages/visitors/test/getByteSizeVisitor.test.ts
  40. 83 0
      packages/visitors/test/getDefinedTypeHistogramVisitor.test.ts
  41. 224 0
      packages/visitors/test/getResolvedInstructionInputsVisitor.test.ts
  42. 126 0
      packages/visitors/test/setStructDefaultValuesVisitor.test.ts
  43. 299 0
      packages/visitors/test/updateAccountsVisitor.test.ts
  44. 201 0
      packages/visitors/test/updateInstructionsVisitor.test.ts
  45. 10 0
      packages/visitors/tsconfig.declarations.json
  46. 7 0
      packages/visitors/tsconfig.json
  47. 9 0
      pnpm-lock.yaml

+ 1 - 1
packages/visitors-core/package.json

@@ -32,7 +32,7 @@
         "solana",
         "framework",
         "standard",
-        "specifications"
+        "visitors"
     ],
     "scripts": {
         "build": "rimraf dist && pnpm build:src && pnpm build:types",

+ 332 - 0
packages/visitors-core/test/NodeSelector.test.ts

@@ -0,0 +1,332 @@
+import {
+    accountNode,
+    booleanTypeNode,
+    definedTypeLinkNode,
+    definedTypeNode,
+    enumEmptyVariantTypeNode,
+    enumStructVariantTypeNode,
+    enumTypeNode,
+    errorNode,
+    instructionAccountNode,
+    instructionArgumentNode,
+    instructionNode,
+    isNode,
+    Node,
+    numberTypeNode,
+    optionTypeNode,
+    programNode,
+    publicKeyTypeNode,
+    rootNode,
+    structFieldTypeNode,
+    structTypeNode,
+} from '@kinobi-so/nodes';
+import test from 'ava';
+
+import {
+    getNodeSelectorFunction,
+    identityVisitor,
+    interceptVisitor,
+    NodeSelector,
+    NodeStack,
+    pipe,
+    recordNodeStackVisitor,
+    visit,
+} from '../src/index.js';
+
+// Given the following tree.
+const tree = rootNode(
+    programNode({
+        accounts: [
+            accountNode({
+                data: structTypeNode([
+                    structFieldTypeNode({
+                        name: 'owner',
+                        type: publicKeyTypeNode(),
+                    }),
+                    structFieldTypeNode({
+                        name: 'mint',
+                        type: publicKeyTypeNode(),
+                    }),
+                    structFieldTypeNode({
+                        name: 'amount',
+                        type: numberTypeNode('u64'),
+                    }),
+                    structFieldTypeNode({
+                        name: 'delegatedAmount',
+                        type: optionTypeNode(numberTypeNode('u64'), {
+                            prefix: numberTypeNode('u32'),
+                        }),
+                    }),
+                ]),
+                name: 'token',
+            }),
+        ],
+        definedTypes: [],
+        errors: [
+            errorNode({
+                code: 0,
+                message: 'Invalid program ID',
+                name: 'invalidProgramId',
+            }),
+            errorNode({
+                code: 1,
+                message: 'Invalid token owner',
+                name: 'invalidTokenOwner',
+            }),
+        ],
+        instructions: [
+            instructionNode({
+                accounts: [
+                    instructionAccountNode({
+                        isSigner: false,
+                        isWritable: true,
+                        name: 'token',
+                    }),
+                    instructionAccountNode({
+                        isSigner: false,
+                        isWritable: true,
+                        name: 'mint',
+                    }),
+                    instructionAccountNode({
+                        isSigner: true,
+                        isWritable: false,
+                        name: 'mintAuthority',
+                    }),
+                ],
+                arguments: [
+                    instructionArgumentNode({
+                        name: 'amount',
+                        type: numberTypeNode('u64'),
+                    }),
+                ],
+                name: 'mintToken',
+            }),
+        ],
+        name: 'splToken',
+        publicKey: '1111',
+        version: '1.0.0',
+    }),
+    [
+        programNode({
+            accounts: [
+                accountNode({
+                    data: structTypeNode([
+                        structFieldTypeNode({
+                            name: 'owner',
+                            type: publicKeyTypeNode(),
+                        }),
+                        structFieldTypeNode({
+                            name: 'opened',
+                            type: booleanTypeNode(numberTypeNode('u64')),
+                        }),
+                        structFieldTypeNode({
+                            name: 'amount',
+                            type: numberTypeNode('u64'),
+                        }),
+                        structFieldTypeNode({
+                            name: 'wrappingPaper',
+                            type: definedTypeLinkNode('wrappingPaper'),
+                        }),
+                    ]),
+                    name: 'gift',
+                }),
+            ],
+            definedTypes: [
+                definedTypeNode({
+                    name: 'wrappingPaper',
+                    type: enumTypeNode([
+                        enumEmptyVariantTypeNode('blue'),
+                        enumEmptyVariantTypeNode('red'),
+                        enumStructVariantTypeNode(
+                            'gold',
+                            structTypeNode([
+                                structFieldTypeNode({
+                                    name: 'owner',
+                                    type: publicKeyTypeNode(),
+                                }),
+                            ]),
+                        ),
+                    ]),
+                }),
+            ],
+            errors: [
+                errorNode({
+                    code: 0,
+                    message: 'Invalid program ID',
+                    name: 'invalidProgramId',
+                }),
+            ],
+            instructions: [
+                instructionNode({
+                    accounts: [
+                        instructionAccountNode({
+                            isSigner: false,
+                            isWritable: true,
+                            name: 'gift',
+                        }),
+                        instructionAccountNode({
+                            isSigner: true,
+                            isWritable: false,
+                            name: 'owner',
+                        }),
+                    ],
+                    arguments: [],
+                    name: 'openGift',
+                }),
+            ],
+            name: 'christmasProgram',
+            publicKey: '2222',
+            version: '1.0.0',
+        }),
+    ],
+);
+
+const macro = test.macro({
+    exec(t, selector: NodeSelector, expectedSelected: Node[]) {
+        // Given a selector function created from the selector.
+        const selectorFunction = getNodeSelectorFunction(selector);
+
+        // And given a visitor that keeps track of selected nodes.
+        const stack = new NodeStack();
+        const selected = [] as Node[];
+        const visitor = pipe(
+            identityVisitor(),
+            v => recordNodeStackVisitor(v, stack),
+            v =>
+                interceptVisitor(v, (node, next) => {
+                    if (selectorFunction(node, stack.clone())) selected.push(node);
+                    return next(node);
+                }),
+        );
+
+        // When we visit the tree.
+        visit(tree, visitor);
+
+        // Then the selected nodes are as expected.
+        t.deepEqual(expectedSelected, selected);
+        selected.forEach((node, index) => t.is(node, expectedSelected[index]));
+    },
+    title(_, selector: NodeSelector) {
+        return typeof selector === 'string'
+            ? `it can select nodes using paths: "${selector}"`
+            : 'it can select nodes using functions';
+    },
+});
+
+/**
+ * [programNode] splToken
+ *     [accountNode] token > [structTypeNode]
+ *         [structFieldTypeNode] owner > [publicKeyTypeNode]
+ *         [structFieldTypeNode] mint > [publicKeyTypeNode]
+ *         [structFieldTypeNode] amount > [numberTypeNode] (u64)
+ *         [structFieldTypeNode] delegatedAmount > [optionTypeNode] (prefix: [numberTypeNode] (u32)) > [numberTypeNode] (u64)
+ *     [instructionNode] mintToken
+ *         [instructionAccountNode] token
+ *         [instructionAccountNode] mint
+ *         [instructionAccountNode] mintAuthority
+ *         [instructionArgumentNode] amount
+ *             [numberTypeNode] (u64)
+ *     [errorNode] invalidProgramId (0)
+ *     [errorNode] invalidTokenOwner (1)
+ * [programNode] christmasProgram
+ *     [accountNode] gift > [structTypeNode]
+ *         [structFieldTypeNode] owner > [publicKeyTypeNode]
+ *         [structFieldTypeNode] opened > [booleanTypeNode] > [numberTypeNode] (u64)
+ *         [structFieldTypeNode] amount > [numberTypeNode] (u64)
+ *         [structFieldTypeNode] wrappingPaper > [definedTypeLinkNode] wrappingPaper
+ *     [instructionNode] openGift
+ *         [instructionAccountNode] gift
+ *         [instructionAccountNode] owner
+ *     [definedTypeNode] wrappingPaper > [enumTypeNode]
+ *         [enumEmptyVariantTypeNode] blue
+ *         [enumEmptyVariantTypeNode] red
+ *         [enumStructVariantTypeNode] gold > [structTypeNode]
+ *             [structFieldTypeNode] owner > [publicKeyTypeNode]
+ *     [errorNode] invalidProgramId (0)
+ */
+
+const splTokenProgram = tree.program;
+const christmasProgram = tree.additionalPrograms[0];
+const tokenAccount = splTokenProgram.accounts[0];
+const tokenDelegatedAmountOption = tokenAccount.data.fields[3].type;
+const mintTokenInstruction = splTokenProgram.instructions[0];
+const giftAccount = christmasProgram.accounts[0];
+const openGiftInstruction = christmasProgram.instructions[0];
+const wrappingPaper = christmasProgram.definedTypes[0];
+const wrappingPaperEnum = wrappingPaper.type;
+const wrappingPaperEnumGold = wrappingPaperEnum.variants[2];
+
+// Select programs.
+test(macro, '[programNode]', [splTokenProgram, christmasProgram]);
+test(macro, '[programNode]splToken', [splTokenProgram]);
+test(macro, 'christmasProgram', [christmasProgram]);
+
+// Select and filter owner nodes.
+test(macro, 'owner', [
+    tokenAccount.data.fields[0],
+    giftAccount.data.fields[0],
+    wrappingPaperEnumGold.struct.fields[0],
+    openGiftInstruction.accounts[1],
+]);
+test(macro, '[structFieldTypeNode]owner', [
+    tokenAccount.data.fields[0],
+    giftAccount.data.fields[0],
+    wrappingPaperEnumGold.struct.fields[0],
+]);
+test(macro, 'splToken.owner', [tokenAccount.data.fields[0]]);
+test(macro, '[instructionNode].owner', [openGiftInstruction.accounts[1]]);
+test(macro, '[accountNode].owner', [tokenAccount.data.fields[0], giftAccount.data.fields[0]]);
+test(macro, '[accountNode]token.owner', [tokenAccount.data.fields[0]]);
+test(macro, 'christmasProgram.[accountNode].owner', [giftAccount.data.fields[0]]);
+test(macro, '[programNode]christmasProgram.[definedTypeNode]wrappingPaper.[enumStructVariantTypeNode]gold.owner', [
+    wrappingPaperEnumGold.struct.fields[0],
+]);
+test(macro, 'christmasProgram.wrappingPaper.gold.owner', [wrappingPaperEnumGold.struct.fields[0]]);
+
+// Select all descendants of a node.
+test(macro, 'wrappingPaper.*', [
+    giftAccount.data.fields[3].type,
+    wrappingPaperEnum,
+    wrappingPaperEnum.variants[0],
+    wrappingPaperEnum.variants[1],
+    wrappingPaperEnum.variants[2],
+    wrappingPaperEnumGold.struct,
+    wrappingPaperEnumGold.struct.fields[0],
+    wrappingPaperEnumGold.struct.fields[0].type,
+]);
+test(macro, 'wrappingPaper.[structFieldTypeNode]', [wrappingPaperEnumGold.struct.fields[0]]);
+test(macro, 'wrappingPaper.blue', [wrappingPaperEnum.variants[0]]);
+test(macro, 'amount.*', [
+    tokenAccount.data.fields[2].type,
+    mintTokenInstruction.arguments[0].type,
+    giftAccount.data.fields[2].type,
+]);
+test(macro, '[instructionNode].amount.*', [mintTokenInstruction.arguments[0].type]);
+test(macro, '[structFieldTypeNode].*', [
+    tokenAccount.data.fields[0].type,
+    tokenAccount.data.fields[1].type,
+    tokenAccount.data.fields[2].type,
+    tokenAccount.data.fields[3].type,
+    tokenDelegatedAmountOption.prefix,
+    tokenDelegatedAmountOption.item,
+    giftAccount.data.fields[0].type,
+    giftAccount.data.fields[1].type,
+    giftAccount.data.fields[1].type.size,
+    giftAccount.data.fields[2].type,
+    giftAccount.data.fields[3].type,
+    wrappingPaperEnumGold.struct.fields[0].type,
+]);
+test(macro, '[structFieldTypeNode].*.*', [
+    tokenDelegatedAmountOption.prefix,
+    tokenDelegatedAmountOption.item,
+    giftAccount.data.fields[1].type.size,
+]);
+
+// Select multiple node kinds.
+test(macro, '[accountNode]gift.[publicKeyTypeNode|booleanTypeNode]', [
+    giftAccount.data.fields[0].type,
+    giftAccount.data.fields[1].type,
+]);
+
+// Select using functions.
+test(macro, node => isNode(node, 'numberTypeNode') && node.format === 'u32', [tokenDelegatedAmountOption.prefix]);

+ 1 - 0
packages/visitors/.gitignore

@@ -0,0 +1 @@
+dist/

+ 1 - 0
packages/visitors/.prettierignore

@@ -0,0 +1 @@
+dist/

+ 23 - 0
packages/visitors/LICENSE

@@ -0,0 +1,23 @@
+MIT License
+
+Copyright (c) 2024 Kinobi
+Copyright (c) 2024 Metaplex Foundation
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

+ 3 - 0
packages/visitors/README.md

@@ -0,0 +1,3 @@
+# Kinobi ➤ Visitors
+
+TODO

+ 68 - 0
packages/visitors/package.json

@@ -0,0 +1,68 @@
+{
+    "name": "@kinobi-so/visitors",
+    "version": "0.20.0",
+    "description": "All visitors for the Kinobi framework",
+    "exports": {
+        "react-native": "./dist/index.react-native.js",
+        "browser": {
+            "import": "./dist/index.browser.js",
+            "require": "./dist/index.browser.cjs"
+        },
+        "node": {
+            "import": "./dist/index.node.js",
+            "require": "./dist/index.node.cjs"
+        },
+        "types": "./dist/types/index.d.ts"
+    },
+    "browser": {
+        "./dist/index.node.cjs": "./dist/index.browser.cjs",
+        "./dist/index.node.js": "./dist/index.browser.js"
+    },
+    "main": "./dist/index.node.cjs",
+    "module": "./dist/index.node.js",
+    "react-native": "./dist/index.react-native.js",
+    "types": "./dist/types/index.d.ts",
+    "type": "module",
+    "files": [
+        "./dist/types",
+        "./dist/index.*"
+    ],
+    "sideEffects": false,
+    "keywords": [
+        "solana",
+        "framework",
+        "standard",
+        "visitors"
+    ],
+    "scripts": {
+        "build": "rimraf dist && pnpm build:src && pnpm build:types",
+        "build:src": "zx ../../node_modules/@kinobi-so/internals/scripts/build-src.mjs package",
+        "build:types": "zx ../../node_modules/@kinobi-so/internals/scripts/build-types.mjs",
+        "dev": "zx ../../node_modules/@kinobi-so/internals/scripts/test-unit.mjs browser --watch",
+        "lint": "zx ../../node_modules/@kinobi-so/internals/scripts/lint.mjs",
+        "lint:fix": "zx ../../node_modules/@kinobi-so/internals/scripts/lint.mjs --fix",
+        "prepublishOnly": "pnpm build",
+        "test": "pnpm test:types && pnpm test:treeshakability && pnpm test:browser && pnpm test:node && pnpm test:react-native",
+        "test:browser": "zx ../../node_modules/@kinobi-so/internals/scripts/test-unit.mjs browser",
+        "test:node": "zx ../../node_modules/@kinobi-so/internals/scripts/test-unit.mjs node",
+        "test:react-native": "zx ../../node_modules/@kinobi-so/internals/scripts/test-unit.mjs react-native",
+        "test:treeshakability": "zx ../../node_modules/@kinobi-so/internals/scripts/test-treeshakability.mjs",
+        "test:types": "zx ../../node_modules/@kinobi-so/internals/scripts/test-types.mjs"
+    },
+    "dependencies": {
+        "@kinobi-so/nodes": "workspace:*",
+        "@kinobi-so/visitors-core": "workspace:*"
+    },
+    "license": "MIT",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/kinobi-so/kinobi"
+    },
+    "bugs": {
+        "url": "http://github.com/kinobi-so/kinobi/issues"
+    },
+    "browserslist": [
+        "supports bigint and not dead",
+        "maintained node versions"
+    ]
+}

+ 30 - 0
packages/visitors/src/addPdasVisitor.ts

@@ -0,0 +1,30 @@
+import { assertIsNode, camelCase, pdaNode, PdaSeedNode, programNode } from '@kinobi-so/nodes';
+import { bottomUpTransformerVisitor } from '@kinobi-so/visitors-core';
+
+export function addPdasVisitor(pdas: Record<string, { name: string; seeds: PdaSeedNode[] }[]>) {
+    return bottomUpTransformerVisitor(
+        Object.entries(pdas).map(([uncasedProgramName, newPdas]) => {
+            const programName = camelCase(uncasedProgramName);
+            return {
+                select: `[programNode]${programName}`,
+                transform: node => {
+                    assertIsNode(node, 'programNode');
+                    const existingPdaNames = new Set(node.pdas.map(pda => pda.name));
+                    const newPdaNames = new Set(newPdas.map(pda => pda.name));
+                    const overlappingPdaNames = new Set([...existingPdaNames].filter(name => newPdaNames.has(name)));
+                    if (overlappingPdaNames.size > 0) {
+                        // TODO: Coded error.
+                        throw new Error(
+                            `Cannot add PDAs to program "${programName}" because the following PDA names ` +
+                                `already exist: ${[...overlappingPdaNames].join(', ')}.`,
+                        );
+                    }
+                    return programNode({
+                        ...node,
+                        pdas: [...node.pdas, ...newPdas.map(pda => pdaNode({ name: pda.name, seeds: pda.seeds }))],
+                    });
+                },
+            };
+        }),
+    );
+}

+ 99 - 0
packages/visitors/src/createSubInstructionsFromEnumArgsVisitor.ts

@@ -0,0 +1,99 @@
+import {
+    assertIsNode,
+    camelCase,
+    EnumTypeNode,
+    instructionArgumentNode,
+    InstructionNode,
+    instructionNode,
+    isNode,
+    numberTypeNode,
+    numberValueNode,
+} from '@kinobi-so/nodes';
+import {
+    BottomUpNodeTransformerWithSelector,
+    bottomUpTransformerVisitor,
+    LinkableDictionary,
+    recordLinkablesVisitor,
+} from '@kinobi-so/visitors-core';
+
+import { flattenInstructionArguments } from './flattenInstructionDataArgumentsVisitor';
+
+export function createSubInstructionsFromEnumArgsVisitor(map: Record<string, string>) {
+    const linkables = new LinkableDictionary();
+
+    const visitor = bottomUpTransformerVisitor(
+        Object.entries(map).map(
+            ([selector, argNameInput]): BottomUpNodeTransformerWithSelector => ({
+                select: ['[instructionNode]', selector],
+                transform: node => {
+                    assertIsNode(node, 'instructionNode');
+
+                    const argFields = node.arguments;
+                    const argName = camelCase(argNameInput);
+                    const argFieldIndex = argFields.findIndex(field => field.name === argName);
+                    const argField = argFieldIndex >= 0 ? argFields[argFieldIndex] : null;
+                    if (!argField) {
+                        // TODO: logWarn
+                        // logWarn(`Could not find instruction argument [${argName}].`);
+                        return node;
+                    }
+
+                    let argType: EnumTypeNode;
+                    if (isNode(argField.type, 'enumTypeNode')) {
+                        argType = argField.type;
+                    } else if (isNode(argField.type, 'definedTypeLinkNode') && linkables.has(argField.type)) {
+                        const linkedType = linkables.get(argField.type)?.type ?? null;
+                        assertIsNode(linkedType, 'enumTypeNode');
+                        argType = linkedType;
+                    } else {
+                        // TODO: logWarn
+                        // logWarn(`Could not find an enum type for ` + `instruction argument [${argName}].`);
+                        return node;
+                    }
+
+                    const subInstructions = argType.variants.map((variant, index): InstructionNode => {
+                        const subName = camelCase(`${node.name} ${variant.name}`);
+                        const subFields = argFields.slice(0, argFieldIndex);
+                        subFields.push(
+                            instructionArgumentNode({
+                                defaultValue: numberValueNode(index),
+                                defaultValueStrategy: 'omitted',
+                                name: `${subName}Discriminator`,
+                                type: numberTypeNode('u8'),
+                            }),
+                        );
+                        if (isNode(variant, 'enumStructVariantTypeNode')) {
+                            subFields.push(
+                                instructionArgumentNode({
+                                    ...argField,
+                                    type: variant.struct,
+                                }),
+                            );
+                        } else if (isNode(variant, 'enumTupleVariantTypeNode')) {
+                            subFields.push(
+                                instructionArgumentNode({
+                                    ...argField,
+                                    type: variant.tuple,
+                                }),
+                            );
+                        }
+                        subFields.push(...argFields.slice(argFieldIndex + 1));
+
+                        return instructionNode({
+                            ...node,
+                            arguments: flattenInstructionArguments(subFields),
+                            name: subName,
+                        });
+                    });
+
+                    return instructionNode({
+                        ...node,
+                        subInstructions: [...(node.subInstructions ?? []), ...subInstructions],
+                    });
+                },
+            }),
+        ),
+    );
+
+    return recordLinkablesVisitor(visitor, linkables);
+}

+ 67 - 0
packages/visitors/src/deduplicateIdenticalDefinedTypesVisitor.ts

@@ -0,0 +1,67 @@
+import { assertIsNode, DefinedTypeNode, getAllPrograms, ProgramNode } from '@kinobi-so/nodes';
+import {
+    deleteNodesVisitor,
+    getUniqueHashStringVisitor,
+    NodeSelector,
+    rootNodeVisitor,
+    visit,
+} from '@kinobi-so/visitors-core';
+
+type DefinedTypeWithProgram = {
+    program: ProgramNode;
+    type: DefinedTypeNode;
+};
+
+export function deduplicateIdenticalDefinedTypesVisitor() {
+    return rootNodeVisitor(root => {
+        const typeMap = new Map<string, DefinedTypeWithProgram[]>();
+
+        // Fill the type map with all defined types.
+        const allPrograms = getAllPrograms(root);
+        allPrograms.forEach(program => {
+            program.definedTypes.forEach(type => {
+                const typeWithProgram = { program, type };
+                const list = typeMap.get(type.name) ?? [];
+                typeMap.set(type.name, [...list, typeWithProgram]);
+            });
+        });
+
+        // Remove all types that are not duplicated.
+        typeMap.forEach((list, name) => {
+            if (list.length <= 1) {
+                typeMap.delete(name);
+            }
+        });
+
+        // Remove duplicates whose types are not equal.
+        const hashVisitor = getUniqueHashStringVisitor({ removeDocs: true });
+        typeMap.forEach((list, name) => {
+            const types = list.map(item => visit(item.type, hashVisitor));
+            const typesAreEqual = types.every((type, _, arr) => type === arr[0]);
+            if (!typesAreEqual) {
+                typeMap.delete(name);
+            }
+        });
+
+        // Get the selectors for all defined types that needs deleting.
+        // Thus, we must select all but the first duplicate of each list.
+        const deleteSelectors = Array.from(typeMap.values())
+            // Order lists by program index, get their tails and flatten.
+            .flatMap(list => {
+                const sortedList = list.sort((a, b) => allPrograms.indexOf(a.program) - allPrograms.indexOf(b.program));
+                const [, ...sortedListTail] = sortedList;
+                return sortedListTail;
+            })
+            // Get selectors from the defined types and their programs.
+            .map(({ program, type }): NodeSelector => `[programNode]${program.name}.[definedTypeNode]${type.name}`);
+
+        // Delete the identified nodes if any.
+        if (deleteSelectors.length > 0) {
+            const newRoot = visit(root, deleteNodesVisitor(deleteSelectors));
+            assertIsNode(newRoot, 'rootNode');
+            return newRoot;
+        }
+
+        return root;
+    });
+}

+ 39 - 0
packages/visitors/src/defaultVisitor.ts

@@ -0,0 +1,39 @@
+import { assertIsNode, Node, RootNode } from '@kinobi-so/nodes';
+import { rootNodeVisitor, visit, Visitor } from '@kinobi-so/visitors-core';
+
+import { deduplicateIdenticalDefinedTypesVisitor } from './deduplicateIdenticalDefinedTypesVisitor';
+import { flattenInstructionDataArgumentsVisitor } from './flattenInstructionDataArgumentsVisitor';
+import { setFixedAccountSizesVisitor } from './setFixedAccountSizesVisitor';
+import {
+    getCommonInstructionAccountDefaultRules,
+    setInstructionAccountDefaultValuesVisitor,
+} from './setInstructionAccountDefaultValuesVisitor';
+import { transformU8ArraysToBytesVisitor } from './transformU8ArraysToBytesVisitor';
+import { unwrapInstructionArgsDefinedTypesVisitor } from './unwrapInstructionArgsDefinedTypesVisitor';
+
+export function defaultVisitor() {
+    return rootNodeVisitor(currentRoot => {
+        let root: RootNode = currentRoot;
+        const updateRoot = (visitor: Visitor<Node | null, 'rootNode'>) => {
+            const newRoot = visit(root, visitor);
+            assertIsNode(newRoot, 'rootNode');
+            root = newRoot;
+        };
+
+        // Defined types.
+        updateRoot(deduplicateIdenticalDefinedTypesVisitor());
+
+        // Accounts.
+        updateRoot(setFixedAccountSizesVisitor());
+
+        // Instructions.
+        updateRoot(setInstructionAccountDefaultValuesVisitor(getCommonInstructionAccountDefaultRules()));
+        updateRoot(unwrapInstructionArgsDefinedTypesVisitor());
+        updateRoot(flattenInstructionDataArgumentsVisitor());
+
+        // Extras.
+        updateRoot(transformU8ArraysToBytesVisitor());
+
+        return root;
+    });
+}

+ 98 - 0
packages/visitors/src/fillDefaultPdaSeedValuesVisitor.ts

@@ -0,0 +1,98 @@
+import {
+    accountValueNode,
+    argumentValueNode,
+    assertIsNode,
+    getAllInstructionArguments,
+    INSTRUCTION_INPUT_VALUE_NODES,
+    InstructionInputValueNode,
+    InstructionNode,
+    isNode,
+    isNodeFilter,
+    PdaNode,
+    PdaSeedValueNode,
+    pdaSeedValueNode,
+    pdaValueNode,
+} from '@kinobi-so/nodes';
+import { extendVisitor, identityVisitor, LinkableDictionary, pipe, Visitor } from '@kinobi-so/visitors-core';
+
+/**
+ * Fills in default values for variable PDA seeds that are not explicitly provided.
+ * Namely, public key seeds are filled with an accountValueNode using the seed name
+ * and other types of seeds are filled with an argumentValueNode using the seed name.
+ *
+ * An instruction and linkable dictionary are required to determine which seeds are
+ * valids and to find the pdaLinkNode for the seed respectively. Any invalid default
+ * seed won't be filled in.
+ *
+ * Strict mode goes one step further and will throw an error if the final array of
+ * pdaSeedValueNodes contains invalid seeds or if there aren't enough variable seeds.
+ */
+export function fillDefaultPdaSeedValuesVisitor(
+    instruction: InstructionNode,
+    linkables: LinkableDictionary,
+    strictMode: boolean = false,
+) {
+    return pipe(identityVisitor(INSTRUCTION_INPUT_VALUE_NODES), v =>
+        extendVisitor(v, {
+            visitPdaValue(node, { next }) {
+                const visitedNode = next(node);
+                assertIsNode(visitedNode, 'pdaValueNode');
+                const foundPda = linkables.get(visitedNode.pda);
+                if (!foundPda) return visitedNode;
+                const seeds = addDefaultSeedValuesFromPdaWhenMissing(instruction, foundPda, visitedNode.seeds);
+                if (strictMode && !allSeedsAreValid(instruction, foundPda, seeds)) {
+                    // TODO: Coded error.
+                    throw new Error(`Invalid seed values for PDA ${foundPda.name} in instruction ${instruction.name}`);
+                }
+                return pdaValueNode(visitedNode.pda, seeds);
+            },
+        }),
+    ) as Visitor<InstructionInputValueNode, InstructionInputValueNode['kind']>;
+}
+
+function addDefaultSeedValuesFromPdaWhenMissing(
+    instruction: InstructionNode,
+    pda: PdaNode,
+    existingSeeds: PdaSeedValueNode[],
+): PdaSeedValueNode[] {
+    const existingSeedNames = new Set(existingSeeds.map(seed => seed.name));
+    const defaultSeeds = getDefaultSeedValuesFromPda(instruction, pda).filter(
+        seed => !existingSeedNames.has(seed.name),
+    );
+    return [...existingSeeds, ...defaultSeeds];
+}
+
+function getDefaultSeedValuesFromPda(instruction: InstructionNode, pda: PdaNode): PdaSeedValueNode[] {
+    return pda.seeds.flatMap((seed): PdaSeedValueNode[] => {
+        if (!isNode(seed, 'variablePdaSeedNode')) return [];
+
+        const hasMatchingAccount = instruction.accounts.some(a => a.name === seed.name);
+        if (isNode(seed.type, 'publicKeyTypeNode') && hasMatchingAccount) {
+            return [pdaSeedValueNode(seed.name, accountValueNode(seed.name))];
+        }
+
+        const hasMatchingArgument = getAllInstructionArguments(instruction).some(a => a.name === seed.name);
+        if (hasMatchingArgument) {
+            return [pdaSeedValueNode(seed.name, argumentValueNode(seed.name))];
+        }
+
+        return [];
+    });
+}
+
+function allSeedsAreValid(instruction: InstructionNode, foundPda: PdaNode, seeds: PdaSeedValueNode[]) {
+    const hasAllVariableSeeds = foundPda.seeds.filter(isNodeFilter('variablePdaSeedNode')).length === seeds.length;
+    const allAccountsName = instruction.accounts.map(a => a.name);
+    const allArgumentsName = getAllInstructionArguments(instruction).map(a => a.name);
+    const validSeeds = seeds.every(seed => {
+        if (isNode(seed.value, 'accountValueNode')) {
+            return allAccountsName.includes(seed.value.name);
+        }
+        if (isNode(seed.value, 'argumentValueNode')) {
+            return allArgumentsName.includes(seed.value.name);
+        }
+        return true;
+    });
+
+    return hasAllVariableSeeds && validSeeds;
+}

+ 58 - 0
packages/visitors/src/flattenInstructionDataArgumentsVisitor.ts

@@ -0,0 +1,58 @@
+import {
+    assertIsNode,
+    camelCase,
+    InstructionArgumentNode,
+    instructionArgumentNode,
+    instructionNode,
+    isNode,
+} from '@kinobi-so/nodes';
+import { bottomUpTransformerVisitor } from '@kinobi-so/visitors-core';
+
+export function flattenInstructionDataArgumentsVisitor() {
+    return bottomUpTransformerVisitor([
+        {
+            select: '[instructionNode]',
+            transform: instruction => {
+                assertIsNode(instruction, 'instructionNode');
+                return instructionNode({
+                    ...instruction,
+                    arguments: flattenInstructionArguments(instruction.arguments),
+                });
+            },
+        },
+    ]);
+}
+
+export type FlattenInstructionArgumentsConfig = string[] | '*';
+
+export const flattenInstructionArguments = (
+    nodes: InstructionArgumentNode[],
+    options: FlattenInstructionArgumentsConfig = '*',
+): InstructionArgumentNode[] => {
+    const camelCaseOptions = options === '*' ? options : options.map(camelCase);
+    const shouldInline = (node: InstructionArgumentNode): boolean =>
+        options === '*' || camelCaseOptions.includes(camelCase(node.name));
+    const inlinedArguments = nodes.flatMap(node => {
+        if (isNode(node.type, 'structTypeNode') && shouldInline(node)) {
+            return node.type.fields.map(field => instructionArgumentNode({ ...field }));
+        }
+        return node;
+    });
+
+    const inlinedFieldsNames = inlinedArguments.map(arg => arg.name);
+    const duplicates = inlinedFieldsNames.filter((e, i, a) => a.indexOf(e) !== i);
+    const uniqueDuplicates = [...new Set(duplicates)];
+    const hasConflictingNames = uniqueDuplicates.length > 0;
+
+    if (hasConflictingNames) {
+        // TODO: logWarn
+        // logWarn(
+        //     `Cound not flatten the attributes of a struct ` +
+        //         `since this would cause the following attributes ` +
+        //         `to conflict [${uniqueDuplicates.join(', ')}].` +
+        //         'You may want to rename the conflicting attributes.',
+        // );
+    }
+
+    return hasConflictingNames ? nodes : inlinedArguments;
+};

+ 53 - 0
packages/visitors/src/flattenStructVisitor.ts

@@ -0,0 +1,53 @@
+import {
+    assertIsNode,
+    camelCase,
+    isNode,
+    Node,
+    StructFieldTypeNode,
+    StructTypeNode,
+    structTypeNode,
+} from '@kinobi-so/nodes';
+import { BottomUpNodeTransformerWithSelector, bottomUpTransformerVisitor } from '@kinobi-so/visitors-core';
+
+export type FlattenStructOptions = string[] | '*';
+
+export function flattenStructVisitor(map: Record<string, FlattenStructOptions>) {
+    return bottomUpTransformerVisitor(
+        Object.entries(map).map(
+            ([stack, options]): BottomUpNodeTransformerWithSelector => ({
+                select: `${stack}.[structTypeNode]`,
+                transform: node => flattenStruct(node, options),
+            }),
+        ),
+    );
+}
+
+export const flattenStruct = (node: Node, options: FlattenStructOptions = '*'): StructTypeNode => {
+    assertIsNode(node, 'structTypeNode');
+    const camelCaseOptions = options === '*' ? options : options.map(camelCase);
+    const shouldInline = (field: StructFieldTypeNode): boolean =>
+        options === '*' || camelCaseOptions.includes(camelCase(field.name));
+    const inlinedFields = node.fields.flatMap(field => {
+        if (isNode(field.type, 'structTypeNode') && shouldInline(field)) {
+            return field.type.fields;
+        }
+        return [field];
+    });
+
+    const inlinedFieldsNames = inlinedFields.map(arg => arg.name);
+    const duplicates = inlinedFieldsNames.filter((e, i, a) => a.indexOf(e) !== i);
+    const uniqueDuplicates = [...new Set(duplicates)];
+    const hasConflictingNames = uniqueDuplicates.length > 0;
+
+    if (hasConflictingNames) {
+        // TODO: logWarn
+        // logWarn(
+        //     `Cound not flatten the attributes of a struct ` +
+        //         `since this would cause the following attributes ` +
+        //         `to conflict [${uniqueDuplicates.join(', ')}].` +
+        //         'You may want to rename the conflicting attributes.',
+        // );
+    }
+
+    return hasConflictingNames ? node : structTypeNode(inlinedFields);
+};

+ 118 - 0
packages/visitors/src/getByteSizeVisitor.ts

@@ -0,0 +1,118 @@
+import { isNode, isScalarEnum, REGISTERED_TYPE_NODE_KINDS, RegisteredTypeNode } from '@kinobi-so/nodes';
+import { LinkableDictionary, mergeVisitor, visit, Visitor } from '@kinobi-so/visitors-core';
+
+export type ByteSizeVisitorKeys =
+    | RegisteredTypeNode['kind']
+    | 'accountNode'
+    | 'definedTypeLinkNode'
+    | 'definedTypeNode'
+    | 'instructionArgumentNode'
+    | 'instructionNode';
+
+export function getByteSizeVisitor(linkables: LinkableDictionary): Visitor<number | null, ByteSizeVisitorKeys> {
+    const visitedDefinedTypes = new Map<string, number | null>();
+    const definedTypeStack: string[] = [];
+
+    const sumSizes = (values: (number | null)[]): number | null =>
+        values.reduce((all, one) => (all === null || one === null ? null : all + one), 0 as number | null);
+
+    const visitor = mergeVisitor(
+        () => null as number | null,
+        (_, values) => sumSizes(values),
+        [
+            ...REGISTERED_TYPE_NODE_KINDS,
+            'definedTypeLinkNode',
+            'definedTypeNode',
+            'accountNode',
+            'instructionNode',
+            'instructionArgumentNode',
+        ],
+    );
+
+    return {
+        ...visitor,
+
+        visitAccount(node) {
+            return visit(node.data, this);
+        },
+
+        visitArrayType(node) {
+            if (!isNode(node.count, 'fixedCountNode')) return null;
+            const fixedSize = node.count.value;
+            const itemSize = visit(node.item, this);
+            const arraySize = itemSize !== null ? itemSize * fixedSize : null;
+            return fixedSize === 0 ? 0 : arraySize;
+        },
+
+        visitDefinedType(node) {
+            if (visitedDefinedTypes.has(node.name)) {
+                return visitedDefinedTypes.get(node.name)!;
+            }
+            definedTypeStack.push(node.name);
+            const child = visit(node.type, this);
+            definedTypeStack.pop();
+            visitedDefinedTypes.set(node.name, child);
+            return child;
+        },
+
+        visitDefinedTypeLink(node) {
+            if (node.importFrom) return null;
+
+            // Fetch the linked type and return null if not found.
+            // The validator visitor will throw a proper error later on.
+            const linkedDefinedType = linkables.get(node);
+            if (!linkedDefinedType) {
+                return null;
+            }
+
+            // This prevents infinite recursion by using assuming
+            // cyclic types don't have a fixed size.
+            if (definedTypeStack.includes(linkedDefinedType.name)) {
+                return null;
+            }
+
+            return visit(linkedDefinedType, this);
+        },
+
+        visitEnumEmptyVariantType() {
+            return 0;
+        },
+
+        visitEnumType(node) {
+            const prefix = visit(node.size, this) ?? 1;
+            if (isScalarEnum(node)) return prefix;
+            const variantSizes = node.variants.map(v => visit(v, this));
+            const allVariantHaveTheSameFixedSize = variantSizes.every((one, _, all) => one === all[0]);
+            return allVariantHaveTheSameFixedSize && variantSizes.length > 0 && variantSizes[0] !== null
+                ? variantSizes[0] + prefix
+                : null;
+        },
+
+        visitFixedSizeType(node) {
+            return node.size;
+        },
+
+        visitInstruction(node) {
+            return sumSizes(node.arguments.map(arg => visit(arg, this)));
+        },
+
+        visitInstructionArgument(node) {
+            return visit(node.type, this);
+        },
+
+        visitNumberType(node) {
+            return parseInt(node.format.slice(1), 10) / 8;
+        },
+
+        visitOptionType(node) {
+            if (!node.fixed) return null;
+            const prefixSize = visit(node.prefix, this) as number;
+            const itemSize = visit(node.item, this);
+            return itemSize !== null ? itemSize + prefixSize : null;
+        },
+
+        visitPublicKeyType() {
+            return 32;
+        },
+    };
+}

+ 96 - 0
packages/visitors/src/getDefinedTypeHistogramVisitor.ts

@@ -0,0 +1,96 @@
+import { CamelCaseString } from '@kinobi-so/nodes';
+import { extendVisitor, interceptVisitor, mergeVisitor, pipe, visit, Visitor } from '@kinobi-so/visitors-core';
+
+export type DefinedTypeHistogram = {
+    [key: CamelCaseString]: {
+        directlyAsInstructionArgs: number;
+        inAccounts: number;
+        inDefinedTypes: number;
+        inInstructionArgs: number;
+        total: number;
+    };
+};
+
+function mergeHistograms(histograms: DefinedTypeHistogram[]): DefinedTypeHistogram {
+    const result: DefinedTypeHistogram = {};
+
+    histograms.forEach(histogram => {
+        Object.keys(histogram).forEach(key => {
+            const mainCaseKey = key as CamelCaseString;
+            if (result[mainCaseKey] === undefined) {
+                result[mainCaseKey] = histogram[mainCaseKey];
+            } else {
+                result[mainCaseKey].total += histogram[mainCaseKey].total;
+                result[mainCaseKey].inAccounts += histogram[mainCaseKey].inAccounts;
+                result[mainCaseKey].inDefinedTypes += histogram[mainCaseKey].inDefinedTypes;
+                result[mainCaseKey].inInstructionArgs += histogram[mainCaseKey].inInstructionArgs;
+                result[mainCaseKey].directlyAsInstructionArgs += histogram[mainCaseKey].directlyAsInstructionArgs;
+            }
+        });
+    });
+
+    return result;
+}
+
+export function getDefinedTypeHistogramVisitor(): Visitor<DefinedTypeHistogram> {
+    let mode: 'account' | 'definedType' | 'instruction' | null = null;
+    let stackLevel = 0;
+
+    return pipe(
+        mergeVisitor(
+            () => ({}) as DefinedTypeHistogram,
+            (_, histograms) => mergeHistograms(histograms),
+        ),
+        v =>
+            interceptVisitor(v, (node, next) => {
+                stackLevel += 1;
+                const newNode = next(node);
+                stackLevel -= 1;
+                return newNode;
+            }),
+        v =>
+            extendVisitor(v, {
+                visitAccount(node, { self }) {
+                    mode = 'account';
+                    stackLevel = 0;
+                    const histogram = visit(node.data, self);
+                    mode = null;
+                    return histogram;
+                },
+
+                visitDefinedType(node, { self }) {
+                    mode = 'definedType';
+                    stackLevel = 0;
+                    const histogram = visit(node.type, self);
+                    mode = null;
+                    return histogram;
+                },
+
+                visitDefinedTypeLink(node) {
+                    if (node.importFrom) {
+                        return {};
+                    }
+
+                    return {
+                        [node.name]: {
+                            directlyAsInstructionArgs: Number(mode === 'instruction' && stackLevel <= 1),
+                            inAccounts: Number(mode === 'account'),
+                            inDefinedTypes: Number(mode === 'definedType'),
+                            inInstructionArgs: Number(mode === 'instruction'),
+                            total: 1,
+                        },
+                    };
+                },
+
+                visitInstruction(node, { self }) {
+                    mode = 'instruction';
+                    stackLevel = 0;
+                    const dataHistograms = node.arguments.map(arg => visit(arg, self));
+                    const extraHistograms = (node.extraArguments ?? []).map(arg => visit(arg, self));
+                    mode = null;
+                    const subHistograms = (node.subInstructions ?? []).map(ix => visit(ix, self));
+                    return mergeHistograms([...dataHistograms, ...extraHistograms, ...subHistograms]);
+                },
+            }),
+    );
+}

+ 245 - 0
packages/visitors/src/getResolvedInstructionInputsVisitor.ts

@@ -0,0 +1,245 @@
+/* eslint-disable no-case-declarations */
+import {
+    AccountValueNode,
+    accountValueNode,
+    ArgumentValueNode,
+    CamelCaseString,
+    getAllInstructionArguments,
+    InstructionAccountNode,
+    InstructionArgumentNode,
+    InstructionInputValueNode,
+    InstructionNode,
+    isNode,
+    VALUE_NODES,
+} from '@kinobi-so/nodes';
+import { singleNodeVisitor, Visitor } from '@kinobi-so/visitors-core';
+
+export type ResolvedInstructionInput = ResolvedInstructionAccount | ResolvedInstructionArgument;
+export type ResolvedInstructionAccount = InstructionAccountNode & {
+    dependsOn: InstructionDependency[];
+    isPda: boolean;
+    resolvedIsOptional: boolean;
+    resolvedIsSigner: boolean | 'either';
+};
+export type ResolvedInstructionArgument = InstructionArgumentNode & {
+    dependsOn: InstructionDependency[];
+};
+type InstructionInput = InstructionAccountNode | InstructionArgumentNode;
+type InstructionDependency = AccountValueNode | ArgumentValueNode;
+
+export function getResolvedInstructionInputsVisitor(
+    options: { includeDataArgumentValueNodes?: boolean } = {},
+): Visitor<ResolvedInstructionInput[], 'instructionNode'> {
+    const includeDataArgumentValueNodes = options.includeDataArgumentValueNodes ?? false;
+    let stack: InstructionInput[] = [];
+    let resolved: ResolvedInstructionInput[] = [];
+    let visitedAccounts = new Map<string, ResolvedInstructionAccount>();
+    let visitedArgs = new Map<string, ResolvedInstructionArgument>();
+
+    function resolveInstructionInput(instruction: InstructionNode, input: InstructionInput): void {
+        // Ensure we don't visit the same input twice.
+        if (
+            (isNode(input, 'instructionAccountNode') && visitedAccounts.has(input.name)) ||
+            (isNode(input, 'instructionArgumentNode') && visitedArgs.has(input.name))
+        ) {
+            return;
+        }
+
+        // Ensure we don't have a circular dependency.
+        const isCircular = stack.some(({ kind, name }) => kind === input.kind && name === input.name);
+        if (isCircular) {
+            const cycle = [...stack.map(({ name }) => name), input.name].join(' -> ');
+            const error =
+                `Circular dependency detected in the accounts and args of ` +
+                `the "${instruction.name}" instruction. ` +
+                `Got the following dependency cycle: ${cycle}.`;
+            throw new Error(error);
+        }
+
+        // Resolve whilst keeping track of the stack.
+        stack.push(input);
+        const localResolved =
+            input.kind === 'instructionAccountNode'
+                ? resolveInstructionAccount(instruction, input)
+                : resolveInstructionArgument(instruction, input);
+        stack.pop();
+
+        // Store the resolved input.
+        resolved.push(localResolved);
+        if (localResolved.kind === 'instructionAccountNode') {
+            visitedAccounts.set(input.name, localResolved);
+        } else {
+            visitedArgs.set(input.name, localResolved);
+        }
+    }
+
+    function resolveInstructionAccount(
+        instruction: InstructionNode,
+        account: InstructionAccountNode,
+    ): ResolvedInstructionAccount {
+        // Find and visit dependencies first.
+        const dependsOn = getInstructionDependencies(account);
+        resolveInstructionDependencies(instruction, account, dependsOn);
+
+        const localResolved: ResolvedInstructionAccount = {
+            ...account,
+            dependsOn,
+            isPda: getAllInstructionArguments(instruction).some(
+                argument =>
+                    isNode(argument.defaultValue, 'accountBumpValueNode') &&
+                    argument.defaultValue.name === account.name,
+            ),
+            resolvedIsOptional: account.isOptional,
+            resolvedIsSigner: account.isSigner,
+        };
+
+        switch (localResolved.defaultValue?.kind) {
+            case 'accountValueNode':
+                const defaultAccount = visitedAccounts.get(localResolved.defaultValue.name)!;
+                const resolvedIsPublicKey = account.isSigner === false && defaultAccount.isSigner === false;
+                const resolvedIsSigner = account.isSigner === true && defaultAccount.isSigner === true;
+                const resolvedIsOptionalSigner = !resolvedIsPublicKey && !resolvedIsSigner;
+                localResolved.resolvedIsSigner = resolvedIsOptionalSigner ? 'either' : resolvedIsSigner;
+                localResolved.resolvedIsOptional = defaultAccount.isOptional;
+                break;
+            case 'publicKeyValueNode':
+            case 'programLinkNode':
+            case 'programIdValueNode':
+                localResolved.resolvedIsSigner = account.isSigner === false ? false : 'either';
+                localResolved.resolvedIsOptional = false;
+                break;
+            case 'pdaValueNode':
+                localResolved.resolvedIsSigner = account.isSigner === false ? false : 'either';
+                localResolved.resolvedIsOptional = false;
+                const { seeds } = localResolved.defaultValue;
+                seeds.forEach(seed => {
+                    if (!isNode(seed.value, 'accountValueNode')) return;
+                    const dependency = visitedAccounts.get(seed.value.name)!;
+                    if (dependency.resolvedIsOptional) {
+                        const error =
+                            `Cannot use optional account "${seed.value.name}" as the "${seed.name}" PDA seed ` +
+                            `for the "${account.name}" account of the "${instruction.name}" instruction.`;
+                        throw new Error(error);
+                    }
+                });
+                break;
+            case 'identityValueNode':
+            case 'payerValueNode':
+            case 'resolverValueNode':
+                localResolved.resolvedIsOptional = false;
+                break;
+            default:
+                break;
+        }
+
+        return localResolved;
+    }
+
+    function resolveInstructionArgument(
+        instruction: InstructionNode,
+        argument: InstructionArgumentNode,
+    ): ResolvedInstructionArgument {
+        // Find and visit dependencies first.
+        const dependsOn = getInstructionDependencies(argument);
+        resolveInstructionDependencies(instruction, argument, dependsOn);
+
+        return { ...argument, dependsOn };
+    }
+
+    function resolveInstructionDependencies(
+        instruction: InstructionNode,
+        parent: InstructionInput,
+        dependencies: InstructionDependency[],
+    ): void {
+        dependencies.forEach(dependency => {
+            let input: InstructionInput | null = null;
+            if (isNode(dependency, 'accountValueNode')) {
+                const dependencyAccount = instruction.accounts.find(a => a.name === dependency.name);
+                if (!dependencyAccount) {
+                    const error =
+                        `Account "${dependency.name}" is not a valid dependency of ${parent.kind} ` +
+                        `"${parent.name}" in the "${instruction.name}" instruction.`;
+                    throw new Error(error);
+                }
+                input = { ...dependencyAccount };
+            } else if (isNode(dependency, 'argumentValueNode')) {
+                const dependencyArgument = getAllInstructionArguments(instruction).find(
+                    a => a.name === dependency.name,
+                );
+                if (!dependencyArgument) {
+                    const error =
+                        `Argument "${dependency.name}" is not a valid dependency of ${parent.kind} ` +
+                        `"${parent.name}" in the "${instruction.name}" instruction.`;
+                    throw new Error(error);
+                }
+                input = { ...dependencyArgument };
+            }
+            if (input) {
+                resolveInstructionInput(instruction, input);
+            }
+        });
+    }
+
+    function getInstructionDependencies(input: InstructionInput): InstructionDependency[] {
+        if (!input.defaultValue) return [];
+
+        const getNestedDependencies = (
+            defaultValue: InstructionInputValueNode | undefined,
+        ): InstructionDependency[] => {
+            if (!defaultValue) return [];
+            return getInstructionDependencies({ ...input, defaultValue });
+        };
+
+        if (isNode(input.defaultValue, ['accountValueNode', 'accountBumpValueNode'])) {
+            return [accountValueNode(input.defaultValue.name)];
+        }
+
+        if (isNode(input.defaultValue, 'pdaValueNode')) {
+            const dependencies = new Map<CamelCaseString, InstructionDependency>();
+            input.defaultValue.seeds.forEach(seed => {
+                if (isNode(seed.value, ['accountValueNode', 'argumentValueNode'])) {
+                    dependencies.set(seed.value.name, { ...seed.value });
+                }
+            });
+            return [...dependencies.values()];
+        }
+
+        if (isNode(input.defaultValue, 'resolverValueNode')) {
+            return input.defaultValue.dependsOn ?? [];
+        }
+
+        if (isNode(input.defaultValue, 'conditionalValueNode')) {
+            return [
+                ...getNestedDependencies(input.defaultValue.condition),
+                ...getNestedDependencies(input.defaultValue.ifTrue),
+                ...getNestedDependencies(input.defaultValue.ifFalse),
+            ];
+        }
+
+        return [];
+    }
+
+    return singleNodeVisitor('instructionNode', (node): ResolvedInstructionInput[] => {
+        // Ensure we always start with a clean slate.
+        stack = [];
+        resolved = [];
+        visitedAccounts = new Map();
+        visitedArgs = new Map();
+
+        const inputs: InstructionInput[] = [
+            ...node.accounts,
+            ...node.arguments.filter(a => {
+                if (includeDataArgumentValueNodes) return a.defaultValue;
+                return a.defaultValue && !isNode(a.defaultValue, VALUE_NODES);
+            }),
+            ...(node.extraArguments ?? []).filter(a => a.defaultValue),
+        ];
+
+        // Visit all instruction accounts.
+        inputs.forEach(input => {
+            resolveInstructionInput(node, input);
+        });
+
+        return resolved;
+    });
+}

+ 29 - 0
packages/visitors/src/index.ts

@@ -0,0 +1,29 @@
+export * from '@kinobi-so/visitors-core';
+
+export * from './addPdasVisitor';
+export * from './createSubInstructionsFromEnumArgsVisitor';
+export * from './deduplicateIdenticalDefinedTypesVisitor';
+export * from './defaultVisitor';
+export * from './fillDefaultPdaSeedValuesVisitor';
+export * from './flattenInstructionDataArgumentsVisitor';
+export * from './flattenStructVisitor';
+export * from './getByteSizeVisitor';
+export * from './getDefinedTypeHistogramVisitor';
+export * from './getResolvedInstructionInputsVisitor';
+export * from './setAccountDiscriminatorFromFieldVisitor';
+export * from './setFixedAccountSizesVisitor';
+export * from './setInstructionAccountDefaultValuesVisitor';
+export * from './setInstructionDiscriminatorsVisitor';
+export * from './setNumberWrappersVisitor';
+export * from './setStructDefaultValuesVisitor';
+export * from './transformDefinedTypesIntoAccountsVisitor';
+export * from './transformU8ArraysToBytesVisitor';
+export * from './unwrapDefinedTypesVisitor';
+export * from './unwrapInstructionArgsDefinedTypesVisitor';
+export * from './unwrapTupleEnumWithSingleStructVisitor';
+export * from './unwrapTypeDefinedLinksVisitor';
+export * from './updateAccountsVisitor';
+export * from './updateDefinedTypesVisitor';
+export * from './updateErrorsVisitor';
+export * from './updateInstructionsVisitor';
+export * from './updateProgramsVisitor';

+ 35 - 0
packages/visitors/src/renameHelpers.ts

@@ -0,0 +1,35 @@
+import {
+    enumEmptyVariantTypeNode,
+    enumStructVariantTypeNode,
+    enumTupleVariantTypeNode,
+    EnumTypeNode,
+    enumTypeNode,
+    EnumVariantTypeNode,
+    isNode,
+    structFieldTypeNode,
+    StructTypeNode,
+    structTypeNode,
+} from '@kinobi-so/nodes';
+
+export function renameStructNode(node: StructTypeNode, map: Record<string, string>): StructTypeNode {
+    return structTypeNode(
+        node.fields.map(field => (map[field.name] ? structFieldTypeNode({ ...field, name: map[field.name] }) : field)),
+    );
+}
+
+export function renameEnumNode(node: EnumTypeNode, map: Record<string, string>): EnumTypeNode {
+    return enumTypeNode(
+        node.variants.map(variant => (map[variant.name] ? renameEnumVariant(variant, map[variant.name]) : variant)),
+        { ...node },
+    );
+}
+
+function renameEnumVariant(variant: EnumVariantTypeNode, newName: string) {
+    if (isNode(variant, 'enumStructVariantTypeNode')) {
+        return enumStructVariantTypeNode(newName, variant.struct);
+    }
+    if (isNode(variant, 'enumTupleVariantTypeNode')) {
+        return enumTupleVariantTypeNode(newName, variant.tuple);
+    }
+    return enumEmptyVariantTypeNode(newName);
+}

+ 49 - 0
packages/visitors/src/setAccountDiscriminatorFromFieldVisitor.ts

@@ -0,0 +1,49 @@
+import {
+    accountNode,
+    assertIsNode,
+    fieldDiscriminatorNode,
+    resolveNestedTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+    transformNestedTypeNode,
+    ValueNode,
+} from '@kinobi-so/nodes';
+import { BottomUpNodeTransformerWithSelector, bottomUpTransformerVisitor } from '@kinobi-so/visitors-core';
+
+export function setAccountDiscriminatorFromFieldVisitor(
+    map: Record<string, { field: string; offset?: number; value: ValueNode }>,
+) {
+    return bottomUpTransformerVisitor(
+        Object.entries(map).map(
+            ([selector, { field, value, offset }]): BottomUpNodeTransformerWithSelector => ({
+                select: ['[accountNode]', selector],
+                transform: node => {
+                    assertIsNode(node, 'accountNode');
+
+                    const accountData = resolveNestedTypeNode(node.data);
+                    const fieldIndex = accountData.fields.findIndex(f => f.name === field);
+                    if (fieldIndex < 0) {
+                        throw new Error(`Account [${node.name}] does not have a field named [${field}].`);
+                    }
+
+                    const fieldNode = accountData.fields[fieldIndex];
+                    return accountNode({
+                        ...node,
+                        data: transformNestedTypeNode(node.data, () =>
+                            structTypeNode([
+                                ...accountData.fields.slice(0, fieldIndex),
+                                structFieldTypeNode({
+                                    ...fieldNode,
+                                    defaultValue: value,
+                                    defaultValueStrategy: 'omitted',
+                                }),
+                                ...accountData.fields.slice(fieldIndex + 1),
+                            ]),
+                        ),
+                        discriminators: [fieldDiscriminatorNode(field, offset), ...(node.discriminators ?? [])],
+                    });
+                },
+            }),
+        ),
+    );
+}

+ 26 - 0
packages/visitors/src/setFixedAccountSizesVisitor.ts

@@ -0,0 +1,26 @@
+import { accountNode, assertIsNode, isNode } from '@kinobi-so/nodes';
+import { LinkableDictionary, recordLinkablesVisitor, topDownTransformerVisitor, visit } from '@kinobi-so/visitors-core';
+
+import { getByteSizeVisitor } from './getByteSizeVisitor';
+
+export function setFixedAccountSizesVisitor() {
+    const linkables = new LinkableDictionary();
+    const byteSizeVisitor = getByteSizeVisitor(linkables);
+
+    const visitor = topDownTransformerVisitor(
+        [
+            {
+                select: node => isNode(node, 'accountNode') && node.size === undefined,
+                transform: node => {
+                    assertIsNode(node, 'accountNode');
+                    const size = visit(node.data, byteSizeVisitor);
+                    if (size === null) return node;
+                    return accountNode({ ...node, size }) as typeof node;
+                },
+            },
+        ],
+        ['rootNode', 'programNode', 'accountNode'],
+    );
+
+    return recordLinkablesVisitor(visitor, linkables);
+}

+ 196 - 0
packages/visitors/src/setInstructionAccountDefaultValuesVisitor.ts

@@ -0,0 +1,196 @@
+import {
+    camelCase,
+    identityValueNode,
+    InstructionAccountNode,
+    InstructionInputValueNode,
+    InstructionNode,
+    instructionNode,
+    payerValueNode,
+    programIdValueNode,
+    publicKeyValueNode,
+} from '@kinobi-so/nodes';
+import {
+    extendVisitor,
+    LinkableDictionary,
+    nonNullableIdentityVisitor,
+    pipe,
+    recordLinkablesVisitor,
+    visit,
+} from '@kinobi-so/visitors-core';
+
+import { fillDefaultPdaSeedValuesVisitor } from './fillDefaultPdaSeedValuesVisitor';
+
+export type InstructionAccountDefaultRule = {
+    /** The name of the instruction account or a pattern to match on it. */
+    account: RegExp | string;
+    /** The default value to assign to it. */
+    defaultValue: InstructionInputValueNode;
+    /** @defaultValue `false`. */
+    ignoreIfOptional?: boolean;
+    /** @defaultValue Defaults to searching accounts on all instructions. */
+    instruction?: string;
+};
+
+export const getCommonInstructionAccountDefaultRules = (): InstructionAccountDefaultRule[] => [
+    {
+        account: /^(payer|feePayer)$/,
+        defaultValue: payerValueNode(),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(authority)$/,
+        defaultValue: identityValueNode(),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(programId)$/,
+        defaultValue: programIdValueNode(),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(systemProgram|splSystemProgram)$/,
+        defaultValue: publicKeyValueNode('11111111111111111111111111111111', 'splSystem'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(tokenProgram|splTokenProgram)$/,
+        defaultValue: publicKeyValueNode('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', 'splToken'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(ataProgram|splAtaProgram)$/,
+        defaultValue: publicKeyValueNode('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL', 'splAssociatedToken'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(tokenMetadataProgram|mplTokenMetadataProgram)$/,
+        defaultValue: publicKeyValueNode('metaqbxxUerdq28cj1RbAWkYQm3ybzjb6a8bt518x1s', 'mplTokenMetadata'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(tokenAuth|mplTokenAuth|authorization|mplAuthorization|auth|mplAuth)RulesProgram$/,
+        defaultValue: publicKeyValueNode('auth9SigNpDKz4sJJ1DfCTuZrZNSAgh9sFD3rboVmgg', 'mplTokenAuthRules'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(candyMachineProgram|mplCandyMachineProgram)$/,
+        defaultValue: publicKeyValueNode('CndyV3LdqHUfDLmE5naZjVN8rBZz4tqhdefbAnjHG3JR', 'mplCandyMachine'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(candyGuardProgram|mplCandyGuardProgram)$/,
+        defaultValue: publicKeyValueNode('Guard1JwRhJkVH6XZhzoYxeBVQe872VH6QggF4BWmS9g', 'mplCandyGuard'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(clockSysvar|sysvarClock)$/,
+        defaultValue: publicKeyValueNode('SysvarC1ock11111111111111111111111111111111'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(epochScheduleSysvar|sysvarEpochSchedule)$/,
+        defaultValue: publicKeyValueNode('SysvarEpochSchedu1e111111111111111111111111'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(instructions?Sysvar|sysvarInstructions?)(Account)?$/,
+        defaultValue: publicKeyValueNode('Sysvar1nstructions1111111111111111111111111'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(recentBlockhashesSysvar|sysvarRecentBlockhashes)$/,
+        defaultValue: publicKeyValueNode('SysvarRecentB1ockHashes11111111111111111111'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(rent|rentSysvar|sysvarRent)$/,
+        defaultValue: publicKeyValueNode('SysvarRent111111111111111111111111111111111'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(rewardsSysvar|sysvarRewards)$/,
+        defaultValue: publicKeyValueNode('SysvarRewards111111111111111111111111111111'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(slotHashesSysvar|sysvarSlotHashes)$/,
+        defaultValue: publicKeyValueNode('SysvarS1otHashes111111111111111111111111111'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(slotHistorySysvar|sysvarSlotHistory)$/,
+        defaultValue: publicKeyValueNode('SysvarS1otHistory11111111111111111111111111'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(stakeHistorySysvar|sysvarStakeHistory)$/,
+        defaultValue: publicKeyValueNode('SysvarStakeHistory1111111111111111111111111'),
+        ignoreIfOptional: true,
+    },
+    {
+        account: /^(mplCoreProgram)$/,
+        defaultValue: publicKeyValueNode('CoREENxT6tW1HoK8ypY1SxRMZTcVPm7R94rH4PZNhX7d', 'mplCore'),
+        ignoreIfOptional: true,
+    },
+];
+
+export function setInstructionAccountDefaultValuesVisitor(rules: InstructionAccountDefaultRule[]) {
+    const linkables = new LinkableDictionary();
+
+    // Place the rules with instructions first.
+    const sortedRules = rules.sort((a, b) => {
+        const ia = 'instruction' in a;
+        const ib = 'instruction' in b;
+        if ((ia && ib) || (!a && !ib)) return 0;
+        return ia ? -1 : 1;
+    });
+
+    function matchRule(
+        instruction: InstructionNode,
+        account: InstructionAccountNode,
+    ): InstructionAccountDefaultRule | undefined {
+        return sortedRules.find(rule => {
+            if ('instruction' in rule && rule.instruction && camelCase(rule.instruction) !== instruction.name) {
+                return false;
+            }
+            return typeof rule.account === 'string'
+                ? camelCase(rule.account) === account.name
+                : rule.account.test(account.name);
+        });
+    }
+
+    return pipe(
+        nonNullableIdentityVisitor(['rootNode', 'programNode', 'instructionNode']),
+        v => recordLinkablesVisitor(v, linkables),
+        v =>
+            extendVisitor(v, {
+                visitInstruction(node) {
+                    const instructionAccounts = node.accounts.map((account): InstructionAccountNode => {
+                        const rule = matchRule(node, account);
+                        if (!rule) return account;
+
+                        if ((rule.ignoreIfOptional ?? false) && (account.isOptional || !!account.defaultValue)) {
+                            return account;
+                        }
+
+                        try {
+                            return {
+                                ...account,
+                                defaultValue: visit(
+                                    rule.defaultValue,
+                                    fillDefaultPdaSeedValuesVisitor(node, linkables, true),
+                                ),
+                            };
+                        } catch (error) {
+                            return account;
+                        }
+                    });
+
+                    return instructionNode({
+                        ...node,
+                        accounts: instructionAccounts,
+                    });
+                },
+            }),
+    );
+}

+ 51 - 0
packages/visitors/src/setInstructionDiscriminatorsVisitor.ts

@@ -0,0 +1,51 @@
+import {
+    assertIsNode,
+    fieldDiscriminatorNode,
+    instructionArgumentNode,
+    instructionNode,
+    numberTypeNode,
+    TypeNode,
+    ValueNode,
+} from '@kinobi-so/nodes';
+import { BottomUpNodeTransformerWithSelector, bottomUpTransformerVisitor } from '@kinobi-so/visitors-core';
+
+type Discriminator = {
+    /** @defaultValue `[]` */
+    docs?: string[];
+    /** @defaultValue `"discriminator"` */
+    name?: string;
+    /** @defaultValue `"omitted"` */
+    strategy?: 'omitted' | 'optional';
+    /** @defaultValue `numberTypeNode('u8')` */
+    type?: TypeNode;
+    value: ValueNode;
+};
+
+export function setInstructionDiscriminatorsVisitor(map: Record<string, Discriminator>) {
+    return bottomUpTransformerVisitor(
+        Object.entries(map).map(
+            ([selector, discriminator]): BottomUpNodeTransformerWithSelector => ({
+                select: ['[instructionNode]', selector],
+                transform: node => {
+                    assertIsNode(node, 'instructionNode');
+                    const discriminatorArgument = instructionArgumentNode({
+                        defaultValue: discriminator.value,
+                        defaultValueStrategy: discriminator.strategy ?? 'omitted',
+                        docs: discriminator.docs ?? [],
+                        name: discriminator.name ?? 'discriminator',
+                        type: discriminator.type ?? numberTypeNode('u8'),
+                    });
+
+                    return instructionNode({
+                        ...node,
+                        arguments: [discriminatorArgument, ...node.arguments],
+                        discriminators: [
+                            fieldDiscriminatorNode(discriminator.name ?? 'discriminator'),
+                            ...(node.discriminators ?? []),
+                        ],
+                    });
+                },
+            }),
+        ),
+    );
+}

+ 32 - 0
packages/visitors/src/setNumberWrappersVisitor.ts

@@ -0,0 +1,32 @@
+import { amountTypeNode, assertIsNestedTypeNode, dateTimeTypeNode, solAmountTypeNode } from '@kinobi-so/nodes';
+import { BottomUpNodeTransformerWithSelector, bottomUpTransformerVisitor } from '@kinobi-so/visitors-core';
+
+export type NumberWrapper =
+    | { decimals: number; kind: 'Amount'; unit?: string }
+    | { kind: 'DateTime' }
+    | { kind: 'SolAmount' };
+
+type NumberWrapperMap = Record<string, NumberWrapper>;
+
+export function setNumberWrappersVisitor(map: NumberWrapperMap) {
+    return bottomUpTransformerVisitor(
+        Object.entries(map).map(
+            ([selectorStack, wrapper]): BottomUpNodeTransformerWithSelector => ({
+                select: `${selectorStack}.[numberTypeNode]`,
+                transform: node => {
+                    assertIsNestedTypeNode(node, 'numberTypeNode');
+                    switch (wrapper.kind) {
+                        case 'DateTime':
+                            return dateTimeTypeNode(node);
+                        case 'SolAmount':
+                            return solAmountTypeNode(node);
+                        case 'Amount':
+                            return amountTypeNode(node, wrapper.decimals, wrapper.unit);
+                        default:
+                            throw new Error(`Invalid number wrapper kind: ${wrapper}`);
+                    }
+                },
+            }),
+        ),
+    );
+}

+ 80 - 0
packages/visitors/src/setStructDefaultValuesVisitor.ts

@@ -0,0 +1,80 @@
+import {
+    assertIsNode,
+    camelCase,
+    InstructionArgumentNode,
+    instructionArgumentNode,
+    instructionNode,
+    StructFieldTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+    ValueNode,
+} from '@kinobi-so/nodes';
+import { BottomUpNodeTransformerWithSelector, bottomUpTransformerVisitor } from '@kinobi-so/visitors-core';
+
+type StructDefaultValueMap = Record<string, Record<string, StructDefaultValue>>;
+type StructDefaultValue = ValueNode | { strategy?: 'omitted' | 'optional'; value: ValueNode } | null;
+
+export function setStructDefaultValuesVisitor(map: StructDefaultValueMap) {
+    return bottomUpTransformerVisitor(
+        Object.entries(map).flatMap(([stack, defaultValues]): BottomUpNodeTransformerWithSelector[] => {
+            const camelCasedDefaultValues = Object.fromEntries(
+                Object.entries(defaultValues).map(([key, value]) => [camelCase(key), value]),
+            );
+
+            return [
+                {
+                    select: `${stack}.[structTypeNode]`,
+                    transform: node => {
+                        assertIsNode(node, 'structTypeNode');
+                        const fields = node.fields.map((field): StructFieldTypeNode => {
+                            const defaultValue = camelCasedDefaultValues[field.name];
+                            if (defaultValue === undefined) return field;
+                            if (defaultValue === null) {
+                                return structFieldTypeNode({
+                                    ...field,
+                                    defaultValue: undefined,
+                                    defaultValueStrategy: undefined,
+                                });
+                            }
+                            return structFieldTypeNode({
+                                ...field,
+                                defaultValue: 'kind' in defaultValue ? defaultValue : defaultValue.value,
+                                defaultValueStrategy: 'kind' in defaultValue ? undefined : defaultValue.strategy,
+                            });
+                        });
+                        return structTypeNode(fields);
+                    },
+                },
+                {
+                    select: ['[instructionNode]', stack],
+                    transform: node => {
+                        assertIsNode(node, 'instructionNode');
+                        const transformArguments = (arg: InstructionArgumentNode): InstructionArgumentNode => {
+                            const defaultValue = camelCasedDefaultValues[arg.name];
+                            if (defaultValue === undefined) return arg;
+                            if (defaultValue === null) {
+                                return instructionArgumentNode({
+                                    ...arg,
+                                    defaultValue: undefined,
+                                    defaultValueStrategy: undefined,
+                                });
+                            }
+                            return instructionArgumentNode({
+                                ...arg,
+                                defaultValue: 'kind' in defaultValue ? defaultValue : defaultValue.value,
+                                defaultValueStrategy: 'kind' in defaultValue ? undefined : defaultValue.strategy,
+                            });
+                        };
+                        return instructionNode({
+                            ...node,
+                            arguments: node.arguments.map(transformArguments),
+                            extraArguments: node.extraArguments
+                                ? node.extraArguments.map(transformArguments)
+                                : undefined,
+                        });
+                    },
+                },
+            ];
+        }),
+    );
+}

+ 30 - 0
packages/visitors/src/transformDefinedTypesIntoAccountsVisitor.ts

@@ -0,0 +1,30 @@
+import { accountNode, assertIsNode, programNode } from '@kinobi-so/nodes';
+import { extendVisitor, nonNullableIdentityVisitor, pipe } from '@kinobi-so/visitors-core';
+
+export function transformDefinedTypesIntoAccountsVisitor(definedTypes: string[]) {
+    return pipe(nonNullableIdentityVisitor(['rootNode', 'programNode']), v =>
+        extendVisitor(v, {
+            visitProgram(program) {
+                const typesToExtract = program.definedTypes.filter(node => definedTypes.includes(node.name));
+
+                const newDefinedTypes = program.definedTypes.filter(node => !definedTypes.includes(node.name));
+
+                const newAccounts = typesToExtract.map(node => {
+                    assertIsNode(node.type, 'structTypeNode');
+                    return accountNode({
+                        ...node,
+                        data: node.type,
+                        discriminators: [],
+                        size: undefined,
+                    });
+                });
+
+                return programNode({
+                    ...program,
+                    accounts: [...program.accounts, ...newAccounts],
+                    definedTypes: newDefinedTypes,
+                });
+            },
+        }),
+    );
+}

+ 37 - 0
packages/visitors/src/transformU8ArraysToBytesVisitor.ts

@@ -0,0 +1,37 @@
+import {
+    ArrayTypeNode,
+    arrayTypeNode,
+    assertIsNode,
+    bytesTypeNode,
+    fixedSizeTypeNode,
+    isNode,
+    TYPE_NODES,
+} from '@kinobi-so/nodes';
+import { extendVisitor, nonNullableIdentityVisitor, pipe, visit } from '@kinobi-so/visitors-core';
+
+export function transformU8ArraysToBytesVisitor(sizes: number[] | '*' = [32, 64]) {
+    const hasRequiredSize = (count: ArrayTypeNode['count']): boolean => {
+        if (!isNode(count, 'fixedCountNode')) return false;
+        return sizes === '*' || sizes.includes(count.value);
+    };
+
+    return pipe(nonNullableIdentityVisitor(), v =>
+        extendVisitor(v, {
+            visitArrayType(node, { self }) {
+                const child = visit(node.item, self);
+                assertIsNode(child, TYPE_NODES);
+
+                if (
+                    isNode(child, 'numberTypeNode') &&
+                    child.format === 'u8' &&
+                    isNode(node.count, 'fixedCountNode') &&
+                    hasRequiredSize(node.count)
+                ) {
+                    return fixedSizeTypeNode(bytesTypeNode(), node.count.value);
+                }
+
+                return arrayTypeNode(child, node.count);
+            },
+        }),
+    );
+}

+ 55 - 0
packages/visitors/src/unwrapDefinedTypesVisitor.ts

@@ -0,0 +1,55 @@
+import { assertIsNodeFilter, camelCase, CamelCaseString, programNode } from '@kinobi-so/nodes';
+import {
+    extendVisitor,
+    LinkableDictionary,
+    nonNullableIdentityVisitor,
+    pipe,
+    recordLinkablesVisitor,
+    visit,
+} from '@kinobi-so/visitors-core';
+
+export function unwrapDefinedTypesVisitor(typesToInline: string[] | '*' = '*') {
+    const linkables = new LinkableDictionary();
+    const typesToInlineMainCased = typesToInline === '*' ? '*' : typesToInline.map(camelCase);
+    const shouldInline = (definedType: CamelCaseString): boolean =>
+        typesToInlineMainCased === '*' || typesToInlineMainCased.includes(definedType);
+
+    return pipe(
+        nonNullableIdentityVisitor(),
+        v => recordLinkablesVisitor(v, linkables),
+        v =>
+            extendVisitor(v, {
+                visitDefinedTypeLink(linkType, { self }) {
+                    if (!shouldInline(linkType.name) || linkType.importFrom) {
+                        return linkType;
+                    }
+
+                    const definedType = linkables.get(linkType);
+                    if (definedType === undefined) {
+                        throw new Error(
+                            `Trying to inline missing defined type [${linkType.name}]. ` +
+                                `Ensure this visitor starts from the root node to access all defined types.`,
+                        );
+                    }
+
+                    return visit(definedType.type, self);
+                },
+
+                visitProgram(program, { self }) {
+                    return programNode({
+                        ...program,
+                        accounts: program.accounts
+                            .map(account => visit(account, self))
+                            .filter(assertIsNodeFilter('accountNode')),
+                        definedTypes: program.definedTypes
+                            .filter(definedType => !shouldInline(definedType.name))
+                            .map(type => visit(type, self))
+                            .filter(assertIsNodeFilter('definedTypeNode')),
+                        instructions: program.instructions
+                            .map(instruction => visit(instruction, self))
+                            .filter(assertIsNodeFilter('instructionNode')),
+                    });
+                },
+            }),
+    );
+}

+ 35 - 0
packages/visitors/src/unwrapInstructionArgsDefinedTypesVisitor.ts

@@ -0,0 +1,35 @@
+import { assertIsNode, CamelCaseString, getAllDefinedTypes, isNode } from '@kinobi-so/nodes';
+import { rootNodeVisitor, visit } from '@kinobi-so/visitors-core';
+
+import { getDefinedTypeHistogramVisitor } from './getDefinedTypeHistogramVisitor';
+import { unwrapDefinedTypesVisitor } from './unwrapDefinedTypesVisitor';
+
+export function unwrapInstructionArgsDefinedTypesVisitor() {
+    return rootNodeVisitor(root => {
+        const histogram = visit(root, getDefinedTypeHistogramVisitor());
+        const allDefinedTypes = getAllDefinedTypes(root);
+
+        const definedTypesToInline: string[] = Object.keys(histogram)
+            // Get all defined types used exactly once as an instruction argument.
+            .filter(
+                name =>
+                    (histogram[name as CamelCaseString].total ?? 0) === 1 &&
+                    (histogram[name as CamelCaseString].directlyAsInstructionArgs ?? 0) === 1,
+            )
+            // Filter out enums which are better defined as external types.
+            .filter(name => {
+                const found = allDefinedTypes.find(type => type.name === name);
+                return found && !isNode(found.type, 'enumTypeNode');
+            });
+
+        // Inline the identified defined types if any.
+        if (definedTypesToInline.length > 0) {
+            const inlineVisitor = unwrapDefinedTypesVisitor(definedTypesToInline);
+            const newRoot = visit(root, inlineVisitor);
+            assertIsNode(newRoot, 'rootNode');
+            return newRoot;
+        }
+
+        return root;
+    });
+}

+ 78 - 0
packages/visitors/src/unwrapTupleEnumWithSingleStructVisitor.ts

@@ -0,0 +1,78 @@
+import {
+    assertIsNode,
+    CamelCaseString,
+    DefinedTypeNode,
+    enumStructVariantTypeNode,
+    EnumTupleVariantTypeNode,
+    getAllDefinedTypes,
+    isNode,
+    resolveNestedTypeNode,
+    StructTypeNode,
+    transformNestedTypeNode,
+} from '@kinobi-so/nodes';
+import {
+    bottomUpTransformerVisitor,
+    getNodeSelectorFunction,
+    NodeSelectorFunction,
+    NodeStack,
+    rootNodeVisitor,
+    visit,
+} from '@kinobi-so/visitors-core';
+
+import { getDefinedTypeHistogramVisitor } from './getDefinedTypeHistogramVisitor';
+import { unwrapDefinedTypesVisitor } from './unwrapDefinedTypesVisitor';
+
+export function unwrapTupleEnumWithSingleStructVisitor(enumsOrVariantsToUnwrap: string[] | '*' = '*') {
+    const selectorFunctions: NodeSelectorFunction[] =
+        enumsOrVariantsToUnwrap === '*'
+            ? [() => true]
+            : enumsOrVariantsToUnwrap.map(selector => getNodeSelectorFunction(selector));
+
+    const shouldUnwrap = (node: EnumTupleVariantTypeNode, stack: NodeStack): boolean =>
+        selectorFunctions.some(selector => selector(node, stack));
+
+    return rootNodeVisitor(root => {
+        const typesToPotentiallyUnwrap: string[] = [];
+        const definedTypes: Map<string, DefinedTypeNode> = new Map(
+            getAllDefinedTypes(root).map(definedType => [definedType.name, definedType]),
+        );
+
+        let newRoot = visit(
+            root,
+            bottomUpTransformerVisitor([
+                {
+                    select: '[enumTupleVariantTypeNode]',
+                    transform: (node, stack) => {
+                        assertIsNode(node, 'enumTupleVariantTypeNode');
+                        if (!shouldUnwrap(node, stack)) return node;
+                        const tupleNode = resolveNestedTypeNode(node.tuple);
+                        if (tupleNode.items.length !== 1) return node;
+                        let item = tupleNode.items[0];
+                        if (isNode(item, 'definedTypeLinkNode')) {
+                            if (item.importFrom) return node;
+                            const definedType = definedTypes.get(item.name);
+                            if (!definedType) return node;
+                            if (!isNode(definedType.type, 'structTypeNode')) return node;
+                            typesToPotentiallyUnwrap.push(item.name);
+                            item = definedType.type;
+                        }
+                        if (!isNode(item, 'structTypeNode')) return node;
+                        const nestedStruct = transformNestedTypeNode(node.tuple, () => item as StructTypeNode);
+                        return enumStructVariantTypeNode(node.name, nestedStruct);
+                    },
+                },
+            ]),
+        );
+        assertIsNode(newRoot, 'rootNode');
+
+        const histogram = visit(newRoot, getDefinedTypeHistogramVisitor());
+        const typesToUnwrap = typesToPotentiallyUnwrap.filter(
+            type => !histogram[type as CamelCaseString] || histogram[type as CamelCaseString].total === 0,
+        );
+
+        newRoot = visit(newRoot, unwrapDefinedTypesVisitor(typesToUnwrap));
+        assertIsNode(newRoot, 'rootNode');
+
+        return newRoot;
+    });
+}

+ 30 - 0
packages/visitors/src/unwrapTypeDefinedLinksVisitor.ts

@@ -0,0 +1,30 @@
+import { assertIsNode } from '@kinobi-so/nodes';
+import {
+    BottomUpNodeTransformerWithSelector,
+    bottomUpTransformerVisitor,
+    LinkableDictionary,
+    pipe,
+    recordLinkablesVisitor,
+} from '@kinobi-so/visitors-core';
+
+export function unwrapTypeDefinedLinksVisitor(definedLinksType: string[]) {
+    const linkables = new LinkableDictionary();
+
+    const transformers: BottomUpNodeTransformerWithSelector[] = definedLinksType.map(selector => ({
+        select: ['[definedTypeLinkNode]', selector],
+        transform: node => {
+            assertIsNode(node, 'definedTypeLinkNode');
+            if (node.importFrom) return node;
+            const definedType = linkables.get(node);
+            if (definedType === undefined) {
+                throw new Error(
+                    `Trying to inline missing defined type [${node.name}]. ` +
+                        `Ensure this visitor starts from the root node to access all defined types.`,
+                );
+            }
+            return definedType.type;
+        },
+    }));
+
+    return pipe(bottomUpTransformerVisitor(transformers), v => recordLinkablesVisitor(v, linkables));
+}

+ 123 - 0
packages/visitors/src/updateAccountsVisitor.ts

@@ -0,0 +1,123 @@
+import {
+    accountLinkNode,
+    accountNode,
+    AccountNodeInput,
+    assertIsNode,
+    camelCase,
+    CamelCaseString,
+    pdaLinkNode,
+    PdaNode,
+    pdaNode,
+    PdaSeedNode,
+    programNode,
+    transformNestedTypeNode,
+} from '@kinobi-so/nodes';
+import { BottomUpNodeTransformerWithSelector, bottomUpTransformerVisitor } from '@kinobi-so/visitors-core';
+
+import { renameStructNode } from './renameHelpers';
+
+export type AccountUpdates =
+    | { delete: true }
+    | (Partial<Omit<AccountNodeInput, 'data'>> & {
+          data?: Record<string, string>;
+          seeds?: PdaSeedNode[];
+      });
+
+export function updateAccountsVisitor(map: Record<string, AccountUpdates>) {
+    return bottomUpTransformerVisitor(
+        Object.entries(map).flatMap(([selector, updates]) => {
+            const newName =
+                typeof updates === 'object' && 'name' in updates && updates.name ? camelCase(updates.name) : undefined;
+            const pdasToUpsert = [] as { pda: PdaNode; program: CamelCaseString }[];
+
+            const transformers: BottomUpNodeTransformerWithSelector[] = [
+                {
+                    select: ['[accountNode]', selector],
+                    transform: (node, stack) => {
+                        assertIsNode(node, 'accountNode');
+                        if ('delete' in updates) return null;
+
+                        const { seeds, pda, ...assignableUpdates } = updates;
+                        let newPda = node.pda;
+                        if (pda && !pda.importFrom && seeds !== undefined) {
+                            newPda = pda;
+                            pdasToUpsert.push({
+                                pda: pdaNode({ name: pda.name, seeds }),
+                                program: stack.getProgram()!.name,
+                            });
+                        } else if (pda) {
+                            newPda = pda;
+                        } else if (seeds !== undefined && node.pda) {
+                            pdasToUpsert.push({
+                                pda: pdaNode({ name: node.pda.name, seeds }),
+                                program: stack.getProgram()!.name,
+                            });
+                        } else if (seeds !== undefined) {
+                            newPda = pdaLinkNode(newName ?? node.name);
+                            pdasToUpsert.push({
+                                pda: pdaNode({ name: newName ?? node.name, seeds }),
+                                program: stack.getProgram()!.name,
+                            });
+                        }
+
+                        return accountNode({
+                            ...node,
+                            ...assignableUpdates,
+                            data: transformNestedTypeNode(node.data, struct =>
+                                renameStructNode(struct, updates.data ?? {}),
+                            ),
+                            pda: newPda,
+                        });
+                    },
+                },
+                {
+                    select: `[programNode]`,
+                    transform: node => {
+                        assertIsNode(node, 'programNode');
+                        const pdasToUpsertForProgram = pdasToUpsert
+                            .filter(p => p.program === node.name)
+                            .map(p => p.pda);
+                        if (pdasToUpsertForProgram.length === 0) return node;
+                        const existingPdaNames = new Set(node.pdas.map(pda => pda.name));
+                        const pdasToCreate = pdasToUpsertForProgram.filter(p => !existingPdaNames.has(p.name));
+                        const pdasToUpdate = new Map(
+                            pdasToUpsertForProgram.filter(p => existingPdaNames.has(p.name)).map(p => [p.name, p]),
+                        );
+                        const newPdas = [...node.pdas.map(p => pdasToUpdate.get(p.name) ?? p), ...pdasToCreate];
+                        return programNode({ ...node, pdas: newPdas });
+                    },
+                },
+            ];
+
+            if (newName) {
+                transformers.push(
+                    {
+                        select: ['[accountLinkNode]', selector],
+                        transform: node => {
+                            assertIsNode(node, 'accountLinkNode');
+                            if (node.importFrom) return node;
+                            return accountLinkNode(newName);
+                        },
+                    },
+                    {
+                        select: ['[pdaNode]', selector],
+                        transform: node => {
+                            assertIsNode(node, 'pdaNode');
+                            return pdaNode({ name: newName, seeds: node.seeds });
+                        },
+                    },
+                    {
+                        select: ['[pdaLinkNode]', selector],
+                        transform: node => {
+                            assertIsNode(node, 'pdaLinkNode');
+                            if (node.importFrom) return node;
+                            return pdaLinkNode(newName);
+                        },
+                    },
+                );
+            }
+
+            return transformers;
+        }),
+    );
+}

+ 64 - 0
packages/visitors/src/updateDefinedTypesVisitor.ts

@@ -0,0 +1,64 @@
+import {
+    assertIsNode,
+    camelCase,
+    definedTypeLinkNode,
+    definedTypeNode,
+    DefinedTypeNodeInput,
+    isNode,
+} from '@kinobi-so/nodes';
+import { BottomUpNodeTransformerWithSelector, bottomUpTransformerVisitor } from '@kinobi-so/visitors-core';
+
+import { renameEnumNode, renameStructNode } from './renameHelpers';
+
+export type DefinedTypeUpdates =
+    | { delete: true }
+    | (Partial<Omit<DefinedTypeNodeInput, 'data'>> & {
+          data?: Record<string, string>;
+      });
+
+export function updateDefinedTypesVisitor(map: Record<string, DefinedTypeUpdates>) {
+    return bottomUpTransformerVisitor(
+        Object.entries(map).flatMap(([selector, updates]): BottomUpNodeTransformerWithSelector[] => {
+            const newName =
+                typeof updates === 'object' && 'name' in updates && updates.name ? camelCase(updates.name) : undefined;
+
+            const transformers: BottomUpNodeTransformerWithSelector[] = [
+                {
+                    select: ['[definedTypeNode]', selector],
+                    transform: node => {
+                        assertIsNode(node, 'definedTypeNode');
+                        if ('delete' in updates) {
+                            return null;
+                        }
+                        const { data: dataUpdates, ...otherUpdates } = updates;
+                        let newType = node.type;
+                        if (isNode(node.type, 'structTypeNode')) {
+                            newType = renameStructNode(node.type, dataUpdates ?? {});
+                        } else if (isNode(node.type, 'enumTypeNode')) {
+                            newType = renameEnumNode(node.type, dataUpdates ?? {});
+                        }
+                        return definedTypeNode({
+                            ...node,
+                            ...otherUpdates,
+                            name: newName ?? node.name,
+                            type: newType,
+                        });
+                    },
+                },
+            ];
+
+            if (newName) {
+                transformers.push({
+                    select: ['[definedTypeLinkNode]', selector],
+                    transform: node => {
+                        assertIsNode(node, 'definedTypeLinkNode');
+                        if (node.importFrom) return node;
+                        return definedTypeLinkNode(newName);
+                    },
+                });
+            }
+
+            return transformers;
+        }),
+    );
+}

+ 19 - 0
packages/visitors/src/updateErrorsVisitor.ts

@@ -0,0 +1,19 @@
+import { assertIsNode, errorNode, ErrorNodeInput } from '@kinobi-so/nodes';
+import { BottomUpNodeTransformerWithSelector, bottomUpTransformerVisitor } from '@kinobi-so/visitors-core';
+
+export type ErrorUpdates = Partial<ErrorNodeInput> | { delete: true };
+
+export function updateErrorsVisitor(map: Record<string, ErrorUpdates>) {
+    return bottomUpTransformerVisitor(
+        Object.entries(map).map(
+            ([name, updates]): BottomUpNodeTransformerWithSelector => ({
+                select: `[errorNode]${name}`,
+                transform: node => {
+                    assertIsNode(node, 'errorNode');
+                    if ('delete' in updates) return null;
+                    return errorNode({ ...node, ...updates });
+                },
+            }),
+        ),
+    );
+}

+ 170 - 0
packages/visitors/src/updateInstructionsVisitor.ts

@@ -0,0 +1,170 @@
+import {
+    assertIsNode,
+    InstructionAccountNode,
+    instructionAccountNode,
+    InstructionAccountNodeInput,
+    InstructionArgumentNode,
+    instructionArgumentNode,
+    InstructionArgumentNodeInput,
+    InstructionInputValueNode,
+    InstructionNode,
+    instructionNode,
+    InstructionNodeInput,
+    TYPE_NODES,
+} from '@kinobi-so/nodes';
+import {
+    BottomUpNodeTransformerWithSelector,
+    bottomUpTransformerVisitor,
+    LinkableDictionary,
+    pipe,
+    recordLinkablesVisitor,
+    visit,
+} from '@kinobi-so/visitors-core';
+
+import { fillDefaultPdaSeedValuesVisitor } from './fillDefaultPdaSeedValuesVisitor';
+
+export type InstructionUpdates =
+    | { delete: true }
+    | (InstructionMetadataUpdates & {
+          accounts?: InstructionAccountUpdates;
+          arguments?: InstructionArgumentUpdates;
+      });
+
+export type InstructionMetadataUpdates = Partial<
+    Omit<
+        InstructionNodeInput,
+        | 'accounts'
+        | 'arguments'
+        | 'byteDeltas'
+        | 'discriminators'
+        | 'extraArguments'
+        | 'remainingAccounts'
+        | 'subInstructions'
+    >
+>;
+
+export type InstructionAccountUpdates = Record<
+    string,
+    Partial<Omit<InstructionAccountNodeInput, 'defaultValue'>> & {
+        defaultValue?: InstructionInputValueNode | null;
+    }
+>;
+
+export type InstructionArgumentUpdates = Record<
+    string,
+    Partial<Omit<InstructionArgumentNodeInput, 'defaultValue'>> & {
+        defaultValue?: InstructionInputValueNode | null;
+    }
+>;
+
+export function updateInstructionsVisitor(map: Record<string, InstructionUpdates>) {
+    const linkables = new LinkableDictionary();
+
+    const transformers = Object.entries(map).map(
+        ([selector, updates]): BottomUpNodeTransformerWithSelector => ({
+            select: ['[instructionNode]', selector],
+            transform: node => {
+                assertIsNode(node, 'instructionNode');
+                if ('delete' in updates) {
+                    return null;
+                }
+
+                const { accounts: accountUpdates, arguments: argumentUpdates, ...metadataUpdates } = updates;
+                const { newArguments, newExtraArguments } = handleInstructionArguments(node, argumentUpdates ?? {});
+                const newAccounts = node.accounts.map(account =>
+                    handleInstructionAccount(node, account, accountUpdates ?? {}, linkables),
+                );
+                return instructionNode({
+                    ...node,
+                    ...metadataUpdates,
+                    accounts: newAccounts,
+                    arguments: newArguments,
+                    extraArguments: newExtraArguments.length > 0 ? newExtraArguments : undefined,
+                });
+            },
+        }),
+    );
+
+    return pipe(bottomUpTransformerVisitor(transformers), v => recordLinkablesVisitor(v, linkables));
+}
+
+function handleInstructionAccount(
+    instruction: InstructionNode,
+    account: InstructionAccountNode,
+    accountUpdates: InstructionAccountUpdates,
+    linkables: LinkableDictionary,
+): InstructionAccountNode {
+    const accountUpdate = accountUpdates?.[account.name];
+    if (!accountUpdate) return account;
+    const { defaultValue, ...acountWithoutDefault } = {
+        ...account,
+        ...accountUpdate,
+    };
+
+    if (!defaultValue) {
+        return instructionAccountNode(acountWithoutDefault);
+    }
+
+    return instructionAccountNode({
+        ...acountWithoutDefault,
+        defaultValue: visit(defaultValue, fillDefaultPdaSeedValuesVisitor(instruction, linkables)),
+    });
+}
+
+function handleInstructionArguments(
+    instruction: InstructionNode,
+    argUpdates: InstructionArgumentUpdates,
+): {
+    newArguments: InstructionArgumentNode[];
+    newExtraArguments: InstructionArgumentNode[];
+} {
+    const usedArguments = new Set<string>();
+
+    const newArguments = instruction.arguments.map(node => {
+        const argUpdate = argUpdates[node.name];
+        if (!argUpdate) return node;
+        usedArguments.add(node.name);
+        return instructionArgumentNode({
+            ...node,
+            defaultValue: argUpdate.defaultValue ?? node.defaultValue,
+            defaultValueStrategy: argUpdate.defaultValueStrategy ?? node.defaultValueStrategy,
+            docs: argUpdate.docs ?? node.docs,
+            name: argUpdate.name ?? node.name,
+            type: argUpdate.type ?? node.type,
+        });
+    });
+
+    const updatedExtraArguments = (instruction.extraArguments ?? []).map(node => {
+        if (usedArguments.has(node.name)) return node;
+        const argUpdate = argUpdates[node.name];
+        if (!argUpdate) return node;
+        usedArguments.add(node.name);
+        return instructionArgumentNode({
+            ...node,
+            defaultValue: argUpdate.defaultValue ?? node.defaultValue,
+            defaultValueStrategy: argUpdate.defaultValueStrategy ?? node.defaultValueStrategy,
+            docs: argUpdate.docs ?? node.docs,
+            name: argUpdate.name ?? node.name,
+            type: argUpdate.type ?? node.type,
+        });
+    });
+
+    const newExtraArguments = [
+        ...updatedExtraArguments,
+        ...Object.entries(argUpdates)
+            .filter(([argName]) => !usedArguments.has(argName))
+            .map(([argName, argUpdate]) => {
+                const { type } = argUpdate;
+                assertIsNode(type, TYPE_NODES);
+                return instructionArgumentNode({
+                    defaultValue: argUpdate.defaultValue ?? undefined,
+                    defaultValueStrategy: argUpdate.defaultValueStrategy ?? undefined,
+                    docs: argUpdate.docs ?? [],
+                    name: argUpdate.name ?? argName,
+                    type,
+                });
+            }),
+    ];
+
+    return { newArguments, newExtraArguments };
+}

+ 39 - 0
packages/visitors/src/updateProgramsVisitor.ts

@@ -0,0 +1,39 @@
+import { assertIsNode, camelCase, programLinkNode, programNode, ProgramNodeInput } from '@kinobi-so/nodes';
+import { BottomUpNodeTransformerWithSelector, bottomUpTransformerVisitor } from '@kinobi-so/visitors-core';
+
+export type ProgramUpdates =
+    | Partial<Omit<ProgramNodeInput, 'accounts' | 'definedTypes' | 'errors' | 'instructions'>>
+    | { delete: true };
+
+export function updateProgramsVisitor(map: Record<string, ProgramUpdates>) {
+    return bottomUpTransformerVisitor(
+        Object.entries(map).flatMap(([name, updates]): BottomUpNodeTransformerWithSelector[] => {
+            const newName =
+                typeof updates === 'object' && 'name' in updates && updates.name ? camelCase(updates.name) : undefined;
+
+            const transformers: BottomUpNodeTransformerWithSelector[] = [
+                {
+                    select: `[programNode]${name}`,
+                    transform: node => {
+                        assertIsNode(node, 'programNode');
+                        if ('delete' in updates) return null;
+                        return programNode({ ...node, ...updates });
+                    },
+                },
+            ];
+
+            if (newName) {
+                transformers.push({
+                    select: `[programLinkNode]${name}`,
+                    transform: node => {
+                        assertIsNode(node, 'programLinkNode');
+                        if (node.importFrom) return node;
+                        return programLinkNode(newName);
+                    },
+                });
+            }
+
+            return transformers;
+        }),
+    );
+}

+ 96 - 0
packages/visitors/test/addPdasVisitor.test.ts

@@ -0,0 +1,96 @@
+import {
+    constantPdaSeedNodeFromProgramId,
+    constantPdaSeedNodeFromString,
+    pdaNode,
+    programNode,
+    publicKeyTypeNode,
+    variablePdaSeedNode,
+} from '@kinobi-so/nodes';
+import { visit } from '@kinobi-so/visitors-core';
+import test from 'ava';
+
+import { addPdasVisitor } from '../src/index.js';
+
+test('it adds PDA nodes to a program', t => {
+    // Given a program with a single PDA.
+    const node = programNode({
+        name: 'myProgram',
+        pdas: [
+            pdaNode({
+                name: 'associatedToken',
+                seeds: [
+                    variablePdaSeedNode('owner', publicKeyTypeNode()),
+                    constantPdaSeedNodeFromProgramId(),
+                    variablePdaSeedNode('mint', publicKeyTypeNode()),
+                ],
+            }),
+        ],
+        publicKey: 'Epo9rxh99jpeeWabRZi4tpgUVxZQeVn9vbbDjUztJtu4',
+    });
+
+    // When we add two more PDAs.
+    const newPdas = [
+        pdaNode({
+            name: 'metadata',
+            seeds: [
+                constantPdaSeedNodeFromString('utf8', 'metadata'),
+                constantPdaSeedNodeFromProgramId(),
+                variablePdaSeedNode('mint', publicKeyTypeNode()),
+            ],
+        }),
+        pdaNode({
+            name: 'masterEdition',
+            seeds: [
+                constantPdaSeedNodeFromString('utf8', 'metadata'),
+                constantPdaSeedNodeFromProgramId(),
+                variablePdaSeedNode('mint', publicKeyTypeNode()),
+                constantPdaSeedNodeFromString('utf8', 'edition'),
+            ],
+        }),
+    ];
+    const result = visit(node, addPdasVisitor({ myProgram: newPdas }));
+
+    // Then we expect the following program to be returned.
+    t.deepEqual(result, { ...node, pdas: [...node.pdas, ...newPdas] });
+});
+
+test('it fails to add a PDA if its name conflicts with an existing PDA on the program', t => {
+    // Given a program with a PDA named "myPda".
+    const node = programNode({
+        name: 'myProgram',
+        pdas: [
+            pdaNode({
+                name: 'myPda',
+                seeds: [
+                    variablePdaSeedNode('owner', publicKeyTypeNode()),
+                    constantPdaSeedNodeFromProgramId(),
+                    variablePdaSeedNode('mint', publicKeyTypeNode()),
+                ],
+            }),
+        ],
+        publicKey: 'Epo9rxh99jpeeWabRZi4tpgUVxZQeVn9vbbDjUztJtu4',
+    });
+
+    // When we try to add another PDA with the same name.
+    const fn = () =>
+        visit(
+            node,
+            addPdasVisitor({
+                myProgram: [
+                    pdaNode({
+                        name: 'myPda',
+                        seeds: [
+                            constantPdaSeedNodeFromString('utf8', 'metadata'),
+                            constantPdaSeedNodeFromProgramId(),
+                            variablePdaSeedNode('mint', publicKeyTypeNode()),
+                        ],
+                    }),
+                ],
+            }),
+        );
+
+    // Then we expect the following error to be thrown.
+    t.throws(fn, {
+        message: 'Cannot add PDAs to program "myProgram" because the following PDA names already exist: myPda.',
+    });
+});

+ 152 - 0
packages/visitors/test/fillDefaultPdaSeedValuesVisitor.test.ts

@@ -0,0 +1,152 @@
+import {
+    accountValueNode,
+    argumentValueNode,
+    conditionalValueNode,
+    instructionAccountNode,
+    instructionArgumentNode,
+    instructionNode,
+    numberTypeNode,
+    numberValueNode,
+    pdaNode,
+    pdaSeedValueNode,
+    pdaValueNode,
+    publicKeyTypeNode,
+    variablePdaSeedNode,
+} from '@kinobi-so/nodes';
+import { LinkableDictionary, visit } from '@kinobi-so/visitors-core';
+import test from 'ava';
+
+import { fillDefaultPdaSeedValuesVisitor } from '../src/index.js';
+
+test('it fills missing pda seed values with default values', t => {
+    // Given a pdaNode with three variable seeds.
+    const pda = pdaNode({
+        name: 'myPda',
+        seeds: [
+            variablePdaSeedNode('seed1', numberTypeNode('u64')),
+            variablePdaSeedNode('seed2', numberTypeNode('u64')),
+            variablePdaSeedNode('seed3', publicKeyTypeNode()),
+        ],
+    });
+
+    // And a linkable dictionary that recorded this PDA.
+    const linkables = new LinkableDictionary();
+    linkables.record(pda);
+
+    // And a pdaValueNode with a single seed filled.
+    const node = pdaValueNode('myPda', [pdaSeedValueNode('seed1', numberValueNode(42))]);
+
+    // And an instruction node that defines both of the missing seeds.
+    const instruction = instructionNode({
+        accounts: [
+            instructionAccountNode({
+                isSigner: false,
+                isWritable: false,
+                name: 'seed3',
+            }),
+        ],
+        arguments: [instructionArgumentNode({ name: 'seed2', type: numberTypeNode('u64') })],
+        name: 'myInstruction',
+    });
+
+    // When we fill the PDA seeds with default values.
+    const result = visit(node, fillDefaultPdaSeedValuesVisitor(instruction, linkables));
+
+    // Then we expect the following pdaValueNode to be returned.
+    t.deepEqual(
+        result,
+        pdaValueNode('myPda', [
+            pdaSeedValueNode('seed1', numberValueNode(42)),
+            pdaSeedValueNode('seed2', argumentValueNode('seed2')),
+            pdaSeedValueNode('seed3', accountValueNode('seed3')),
+        ]),
+    );
+});
+
+test('it fills nested pda value nodes', t => {
+    // Given a pdaNode with three variable seeds.
+    const pda = pdaNode({
+        name: 'myPda',
+        seeds: [
+            variablePdaSeedNode('seed1', numberTypeNode('u64')),
+            variablePdaSeedNode('seed2', numberTypeNode('u64')),
+            variablePdaSeedNode('seed3', publicKeyTypeNode()),
+        ],
+    });
+
+    // And a linkable dictionary that recorded this PDA.
+    const linkables = new LinkableDictionary();
+    linkables.record(pda);
+
+    // And a pdaValueNode nested inside a conditionalValueNode.
+    const node = conditionalValueNode({
+        condition: accountValueNode('myAccount'),
+        ifTrue: pdaValueNode('myPda', [pdaSeedValueNode('seed1', numberValueNode(42))]),
+    });
+
+    // And an instruction node that defines both of the missing seeds.
+    const instruction = instructionNode({
+        accounts: [
+            instructionAccountNode({
+                isSigner: false,
+                isWritable: false,
+                name: 'seed3',
+            }),
+        ],
+        arguments: [instructionArgumentNode({ name: 'seed2', type: numberTypeNode('u64') })],
+        name: 'myInstruction',
+    });
+
+    // When we fill the PDA seeds with default values.
+    const result = visit(node, fillDefaultPdaSeedValuesVisitor(instruction, linkables));
+
+    // Then we expect the following conditionalValueNode to be returned.
+    t.deepEqual(
+        result,
+        conditionalValueNode({
+            condition: accountValueNode('myAccount'),
+            ifTrue: pdaValueNode('myPda', [
+                pdaSeedValueNode('seed1', numberValueNode(42)),
+                pdaSeedValueNode('seed2', argumentValueNode('seed2')),
+                pdaSeedValueNode('seed3', accountValueNode('seed3')),
+            ]),
+        }),
+    );
+});
+
+test('it ignores default seeds missing from the instruction', t => {
+    // Given a pdaNode with three variable seeds.
+    const pda = pdaNode({
+        name: 'myPda',
+        seeds: [
+            variablePdaSeedNode('seed1', numberTypeNode('u64')),
+            variablePdaSeedNode('seed2', numberTypeNode('u64')),
+            variablePdaSeedNode('seed3', publicKeyTypeNode()),
+        ],
+    });
+
+    // And a linkable dictionary that recorded this PDA.
+    const linkables = new LinkableDictionary();
+    linkables.record(pda);
+
+    // And a pdaValueNode with a single seed filled.
+    const node = pdaValueNode('myPda', [pdaSeedValueNode('seed1', numberValueNode(42))]);
+
+    // And an instruction node that defines only seed2 as an argument.
+    const instruction = instructionNode({
+        arguments: [instructionArgumentNode({ name: 'seed2', type: numberTypeNode('u64') })],
+        name: 'myInstruction',
+    });
+
+    // When we fill the PDA seeds with default values.
+    const result = visit(node, fillDefaultPdaSeedValuesVisitor(instruction, linkables));
+
+    // Then we expect the following pdaValueNode to be returned.
+    t.deepEqual(
+        result,
+        pdaValueNode('myPda', [
+            pdaSeedValueNode('seed1', numberValueNode(42)),
+            pdaSeedValueNode('seed2', argumentValueNode('seed2')),
+        ]),
+    );
+});

+ 93 - 0
packages/visitors/test/getByteSizeVisitor.test.ts

@@ -0,0 +1,93 @@
+import {
+    enumEmptyVariantTypeNode,
+    enumStructVariantTypeNode,
+    enumTupleVariantTypeNode,
+    enumTypeNode,
+    fixedSizeTypeNode,
+    Node,
+    numberTypeNode,
+    publicKeyTypeNode,
+    stringTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+    tupleTypeNode,
+} from '@kinobi-so/nodes';
+import { LinkableDictionary, visit, Visitor } from '@kinobi-so/visitors-core';
+import test from 'ava';
+
+import { getByteSizeVisitor } from '../src/index.js';
+
+const macro = test.macro((t, node: Node, expectedSize: number | null) => {
+    t.is(visit(node, getByteSizeVisitor(new LinkableDictionary()) as Visitor<number | null>), expectedSize);
+});
+
+test('it gets the size of public keys', macro, publicKeyTypeNode(), 32);
+test('it gets the size of u8 numbers', macro, numberTypeNode('u8'), 1);
+test('it gets the size of i8 numbers', macro, numberTypeNode('i8'), 1);
+test('it gets the size of u16 numbers', macro, numberTypeNode('u16'), 2);
+test('it gets the size of i16 numbers', macro, numberTypeNode('i16'), 2);
+test('it gets the size of u32 numbers', macro, numberTypeNode('u32'), 4);
+test('it gets the size of i32 numbers', macro, numberTypeNode('i32'), 4);
+test('it gets the size of u64 numbers', macro, numberTypeNode('u64'), 8);
+test('it gets the size of i64 numbers', macro, numberTypeNode('i64'), 8);
+test('it gets the size of u128 numbers', macro, numberTypeNode('u128'), 16);
+test('it gets the size of i128 numbers', macro, numberTypeNode('i128'), 16);
+test('it gets the size of f32 numbers', macro, numberTypeNode('f32'), 4);
+test('it gets the size of f64 numbers', macro, numberTypeNode('f64'), 8);
+
+test(
+    'it gets the size of fixed structs',
+    macro,
+    structTypeNode([
+        structFieldTypeNode({ name: 'age', type: numberTypeNode('u32') }),
+        structFieldTypeNode({
+            name: 'firstname',
+            type: fixedSizeTypeNode(stringTypeNode('utf8'), 42),
+        }),
+    ]),
+    4 + 42,
+);
+test(
+    'it gets the size of variable structs',
+    macro,
+    structTypeNode([
+        structFieldTypeNode({ name: 'age', type: numberTypeNode('u32') }),
+        structFieldTypeNode({ name: 'firstname', type: stringTypeNode('utf8') }),
+    ]),
+    null,
+);
+test(
+    'it gets the size of scalar enums',
+    macro,
+    enumTypeNode([enumEmptyVariantTypeNode('A'), enumEmptyVariantTypeNode('B'), enumEmptyVariantTypeNode('C')], {
+        size: numberTypeNode('u64'),
+    }),
+    8,
+);
+test(
+    'it gets the size of fixed data enums',
+    macro,
+    enumTypeNode(
+        [
+            enumTupleVariantTypeNode('A', tupleTypeNode([numberTypeNode('u32')])),
+            enumStructVariantTypeNode(
+                'B',
+                structTypeNode([
+                    structFieldTypeNode({ name: 'x', type: numberTypeNode('u16') }),
+                    structFieldTypeNode({ name: 'y', type: numberTypeNode('u16') }),
+                ]),
+            ),
+        ],
+        { size: numberTypeNode('u8') },
+    ),
+    1 + 4,
+);
+test(
+    'it gets the size of variable data enums',
+    macro,
+    enumTypeNode([
+        enumEmptyVariantTypeNode('A'),
+        enumTupleVariantTypeNode('B', tupleTypeNode([numberTypeNode('u32')])),
+    ]),
+    null,
+);

+ 83 - 0
packages/visitors/test/getDefinedTypeHistogramVisitor.test.ts

@@ -0,0 +1,83 @@
+import {
+    accountNode,
+    definedTypeLinkNode,
+    definedTypeNode,
+    enumTypeNode,
+    instructionArgumentNode,
+    instructionNode,
+    programNode,
+    structFieldTypeNode,
+    structTypeNode,
+} from '@kinobi-so/nodes';
+import { visit } from '@kinobi-so/visitors-core';
+import test from 'ava';
+
+import { getDefinedTypeHistogramVisitor } from '../src/index.js';
+
+test('it counts the amount of times defined types are used within the tree', t => {
+    // Given the following tree.
+    const node = programNode({
+        accounts: [
+            accountNode({
+                data: structTypeNode([
+                    structFieldTypeNode({
+                        name: 'field1',
+                        type: definedTypeLinkNode('myStruct'),
+                    }),
+                    structFieldTypeNode({
+                        name: 'field2',
+                        type: definedTypeLinkNode('myEnum'),
+                    }),
+                ]),
+                name: 'myAccount',
+            }),
+        ],
+        definedTypes: [
+            definedTypeNode({
+                name: 'myStruct',
+                type: structTypeNode([]),
+            }),
+            definedTypeNode({
+                name: 'myEnum',
+                type: enumTypeNode([]),
+            }),
+        ],
+        errors: [],
+        instructions: [
+            instructionNode({
+                accounts: [],
+                arguments: [
+                    instructionArgumentNode({
+                        name: 'arg1',
+                        type: definedTypeLinkNode('myStruct'),
+                    }),
+                ],
+                name: 'myInstruction',
+            }),
+        ],
+        name: 'customProgram',
+        publicKey: '1111',
+        version: '1.0.0',
+    });
+
+    // When we get its defined type histogram.
+    const histogram = visit(node, getDefinedTypeHistogramVisitor());
+
+    // Then we expect the following histogram.
+    t.deepEqual(histogram, {
+        myEnum: {
+            directlyAsInstructionArgs: 0,
+            inAccounts: 1,
+            inDefinedTypes: 0,
+            inInstructionArgs: 0,
+            total: 1,
+        },
+        myStruct: {
+            directlyAsInstructionArgs: 1,
+            inAccounts: 1,
+            inDefinedTypes: 0,
+            inInstructionArgs: 1,
+            total: 2,
+        },
+    });
+});

+ 224 - 0
packages/visitors/test/getResolvedInstructionInputsVisitor.test.ts

@@ -0,0 +1,224 @@
+import {
+    accountValueNode,
+    instructionAccountNode,
+    instructionArgumentNode,
+    instructionNode,
+    numberTypeNode,
+    publicKeyTypeNode,
+} from '@kinobi-so/nodes';
+import { visit } from '@kinobi-so/visitors-core';
+import test from 'ava';
+
+import { getResolvedInstructionInputsVisitor } from '../src/index.js';
+
+test('it returns all instruction accounts in order of resolution', t => {
+    // Given the following instruction node with an account that defaults to another account.
+    const node = instructionNode({
+        accounts: [
+            instructionAccountNode({
+                defaultValue: accountValueNode('authority'),
+                isSigner: true,
+                isWritable: false,
+                name: 'owner',
+            }),
+            instructionAccountNode({
+                isSigner: true,
+                isWritable: false,
+                name: 'authority',
+            }),
+        ],
+        name: 'myInstruction',
+    });
+
+    // When we get its resolved inputs.
+    const result = visit(node, getResolvedInstructionInputsVisitor());
+
+    // Then we expect the accounts to be in order of resolution.
+    t.deepEqual(result, [
+        {
+            ...node.accounts[1],
+            dependsOn: [],
+            isPda: false,
+            resolvedIsOptional: false,
+            resolvedIsSigner: true,
+        },
+        {
+            ...node.accounts[0],
+            dependsOn: [accountValueNode('authority')],
+            isPda: false,
+            resolvedIsOptional: false,
+            resolvedIsSigner: true,
+        },
+    ]);
+});
+
+test('it sets the resolved signer to either when a non signer defaults to a signer account', t => {
+    // Given the following instruction node such that a non signer account defaults to a signer account.
+    const node = instructionNode({
+        accounts: [
+            instructionAccountNode({
+                defaultValue: accountValueNode('authority'),
+                isSigner: false,
+                isWritable: false,
+                name: 'owner',
+            }),
+            instructionAccountNode({
+                isSigner: true,
+                isWritable: false,
+                name: 'authority',
+            }),
+        ],
+        name: 'myInstruction',
+    });
+
+    // When we get its resolved inputs.
+    const result = visit(node, getResolvedInstructionInputsVisitor());
+
+    // Then we expect the resolved signer to be either for the non signer account.
+    t.deepEqual(result[1], {
+        ...node.accounts[0],
+        dependsOn: [accountValueNode('authority')],
+        isPda: false,
+        resolvedIsOptional: false,
+        resolvedIsSigner: 'either',
+    });
+});
+
+test('it sets the resolved signer to either when a signer defaults to a non signer account', t => {
+    // Given the following instruction node such that a signer account defaults to a non signer account.
+    const node = instructionNode({
+        accounts: [
+            instructionAccountNode({
+                defaultValue: accountValueNode('authority'),
+                isSigner: true,
+                isWritable: false,
+                name: 'owner',
+            }),
+            instructionAccountNode({
+                isSigner: false,
+                isWritable: false,
+                name: 'authority',
+            }),
+        ],
+        name: 'myInstruction',
+    });
+
+    // When we get its resolved inputs.
+    const result = visit(node, getResolvedInstructionInputsVisitor());
+
+    // Then we expect the resolved signer to be either for the signer account.
+    t.deepEqual(result[1], {
+        ...node.accounts[0],
+        dependsOn: [accountValueNode('authority')],
+        isPda: false,
+        resolvedIsOptional: false,
+        resolvedIsSigner: 'either',
+    });
+});
+
+test('it includes instruction data arguments with default values', t => {
+    // Given the following instruction node with two arguments such that:
+    // - The first argument defaults to an account.
+    // - The second argument has no default value.
+    const node = instructionNode({
+        accounts: [
+            instructionAccountNode({
+                isSigner: true,
+                isWritable: false,
+                name: 'owner',
+            }),
+        ],
+        arguments: [
+            instructionArgumentNode({
+                defaultValue: accountValueNode('owner'),
+                name: 'ownerArg',
+                type: publicKeyTypeNode(),
+            }),
+            instructionArgumentNode({
+                name: 'argWithoutDefaults',
+                type: numberTypeNode('u8'),
+            }),
+        ],
+        name: 'myInstruction',
+    });
+
+    // When we get its resolved inputs.
+    const result = visit(node, getResolvedInstructionInputsVisitor());
+
+    // Then we expect the following inputs.
+    t.deepEqual(result, [
+        {
+            ...node.accounts[0],
+            dependsOn: [],
+            isPda: false,
+            resolvedIsOptional: false,
+            resolvedIsSigner: true,
+        },
+        {
+            ...node.arguments[0],
+            dependsOn: [accountValueNode('owner')],
+        },
+    ]);
+
+    // And the argument without default value is not included.
+    t.false(result.some(input => input.name === 'argWithoutDefaults'));
+});
+
+test('it includes instruction extra arguments with default values', t => {
+    // Given the following instruction node with two extra arguments such that:
+    // - The first argument defaults to an account.
+    // - The second argument has no default value.
+    const node = instructionNode({
+        accounts: [
+            instructionAccountNode({
+                isSigner: true,
+                isWritable: false,
+                name: 'owner',
+            }),
+        ],
+        extraArguments: [
+            instructionArgumentNode({
+                defaultValue: accountValueNode('owner'),
+                name: 'ownerArg',
+                type: publicKeyTypeNode(),
+            }),
+            instructionArgumentNode({
+                name: 'argWithoutDefaults',
+                type: numberTypeNode('u8'),
+            }),
+        ],
+        name: 'myInstruction',
+    });
+
+    // When we get its resolved inputs.
+    const result = visit(node, getResolvedInstructionInputsVisitor());
+
+    // Then we expect the following inputs.
+    t.deepEqual(result, [
+        {
+            ...node.accounts[0],
+            dependsOn: [],
+            isPda: false,
+            resolvedIsOptional: false,
+            resolvedIsSigner: true,
+        },
+        {
+            ...node.extraArguments![0],
+            dependsOn: [accountValueNode('owner')],
+        },
+    ]);
+
+    // And the argument without default value is not included.
+    t.false(result.some(input => input.name === 'argWithoutDefaults'));
+});
+
+test('it returns an empty array for empty instructions', t => {
+    // Given the following empty instruction node.
+    const node = instructionNode({ name: 'myInstruction' });
+
+    // When we get its resolved inputs.
+    const result = visit(node, getResolvedInstructionInputsVisitor());
+
+    // Then we expect an empty array.
+    t.deepEqual(result, []);
+});

+ 126 - 0
packages/visitors/test/setStructDefaultValuesVisitor.test.ts

@@ -0,0 +1,126 @@
+import {
+    accountNode,
+    assertIsNode,
+    definedTypeNode,
+    instructionArgumentNode,
+    instructionNode,
+    noneValueNode,
+    numberTypeNode,
+    numberValueNode,
+    optionTypeNode,
+    publicKeyTypeNode,
+    resolveNestedTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+} from '@kinobi-so/nodes';
+import { visit } from '@kinobi-so/visitors-core';
+import test from 'ava';
+
+import { setStructDefaultValuesVisitor } from '../src/index.js';
+
+test('it adds new default values to struct fields', t => {
+    // Given the following person type with no default values.
+    const node = definedTypeNode({
+        name: 'person',
+        type: structTypeNode([
+            structFieldTypeNode({
+                name: 'age',
+                type: numberTypeNode('u32'),
+            }),
+            structFieldTypeNode({
+                name: 'dateOfBirth',
+                type: optionTypeNode(numberTypeNode('i64')),
+            }),
+        ]),
+    });
+
+    // When we set default values for the age and dateOfBirth fields of the person type.
+    const result = visit(
+        node,
+        setStructDefaultValuesVisitor({
+            person: {
+                age: numberValueNode(42),
+                dateOfBirth: noneValueNode(),
+            },
+        }),
+    );
+
+    // Then we expect the following tree changes.
+    assertIsNode(result, 'definedTypeNode');
+    assertIsNode(result.type, 'structTypeNode');
+    t.deepEqual(result.type.fields[0].defaultValue, numberValueNode(42));
+    t.is(result.type.fields[0].defaultValueStrategy, undefined);
+    t.deepEqual(result.type.fields[1].defaultValue, noneValueNode());
+    t.is(result.type.fields[1].defaultValueStrategy, undefined);
+});
+
+test('it adds new default values with custom strategies to struct fields', t => {
+    // Given the following token account with no default values.
+    const node = accountNode({
+        data: structTypeNode([
+            structFieldTypeNode({
+                name: 'discriminator',
+                type: numberTypeNode('u8'),
+            }),
+            structFieldTypeNode({
+                name: 'delegateAuthority',
+                type: optionTypeNode(publicKeyTypeNode()),
+            }),
+        ]),
+        name: 'token',
+    });
+
+    // When we set default values of that account with custom strategies.
+    const result = visit(
+        node,
+        setStructDefaultValuesVisitor({
+            token: {
+                delegateAuthority: { strategy: 'optional', value: noneValueNode() },
+                discriminator: { strategy: 'omitted', value: numberValueNode(42) },
+            },
+        }),
+    );
+
+    // Then we expect the following tree changes.
+    assertIsNode(result, 'accountNode');
+    const data = resolveNestedTypeNode(result.data);
+    t.deepEqual(data.fields[0].defaultValue, numberValueNode(42));
+    t.is(data.fields[0].defaultValueStrategy, 'omitted');
+    t.deepEqual(data.fields[1].defaultValue, noneValueNode());
+    t.is(data.fields[1].defaultValueStrategy, 'optional');
+});
+
+test('it adds new default values to instruction arguments', t => {
+    // Given the following instruction node with no default values for its arguments
+    const node = instructionNode({
+        arguments: [
+            instructionArgumentNode({
+                name: 'discriminator',
+                type: numberTypeNode('u8'),
+            }),
+            instructionArgumentNode({
+                name: 'amount',
+                type: numberTypeNode('u64'),
+            }),
+        ],
+        name: 'transferTokens',
+    });
+
+    // When we set default values for its arguments.
+    const result = visit(
+        node,
+        setStructDefaultValuesVisitor({
+            transferTokens: {
+                amount: numberValueNode(1),
+                discriminator: { strategy: 'omitted', value: numberValueNode(42) },
+            },
+        }),
+    );
+
+    // Then we expect the following tree changes.
+    assertIsNode(result, 'instructionNode');
+    t.deepEqual(result.arguments[0].defaultValue, numberValueNode(42));
+    t.is(result.arguments[0].defaultValueStrategy, 'omitted');
+    t.deepEqual(result.arguments[1].defaultValue, numberValueNode(1));
+    t.is(result.arguments[1].defaultValueStrategy, undefined);
+});

+ 299 - 0
packages/visitors/test/updateAccountsVisitor.test.ts

@@ -0,0 +1,299 @@
+import {
+    accountNode,
+    assertIsNode,
+    CamelCaseString,
+    constantPdaSeedNodeFromString,
+    numberTypeNode,
+    pdaLinkNode,
+    pdaNode,
+    programNode,
+    resolveNestedTypeNode,
+    rootNode,
+    structFieldTypeNode,
+    structTypeNode,
+} from '@kinobi-so/nodes';
+import { visit } from '@kinobi-so/visitors-core';
+import test from 'ava';
+
+import { updateAccountsVisitor } from '../src/index.js';
+
+test('it updates the name of an account', t => {
+    // Given the following program node with one account.
+    const node = programNode({
+        accounts: [accountNode({ name: 'myAccount' })],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // When we update the name of that account.
+    const result = visit(
+        node,
+        updateAccountsVisitor({
+            myAccount: { name: 'myNewAccount' },
+        }),
+    );
+
+    // Then we expect the following tree changes.
+    assertIsNode(result, 'programNode');
+    t.is(result.accounts[0].name, 'myNewAccount' as CamelCaseString);
+});
+
+test('it updates the name of an account within a specific program', t => {
+    // Given two programs each with an account of the same name.
+    const node = rootNode(
+        programNode({
+            accounts: [accountNode({ name: 'candyMachine' })],
+            name: 'myProgramA',
+            publicKey: '1111',
+        }),
+        [
+            programNode({
+                accounts: [accountNode({ name: 'candyMachine' })],
+                name: 'myProgramB',
+                publicKey: '2222',
+            }),
+        ],
+    );
+
+    // When we update the name of that account in the first program.
+    const result = visit(
+        node,
+        updateAccountsVisitor({
+            'myProgramA.candyMachine': { name: 'newCandyMachine' },
+        }),
+    );
+
+    // Then we expect the first account to have been renamed.
+    assertIsNode(result, 'rootNode');
+    t.is(result.program.accounts[0].name, 'newCandyMachine' as CamelCaseString);
+
+    // But not the second account.
+    t.is(result.additionalPrograms[0].accounts[0].name, 'candyMachine' as CamelCaseString);
+});
+
+test("it renames the fields of an account's data", t => {
+    // Given the following account.
+    const node = accountNode({
+        data: structTypeNode([structFieldTypeNode({ name: 'myData', type: numberTypeNode('u32') })]),
+        name: 'myAccount',
+    });
+
+    // When we rename its data fields.
+    const result = visit(
+        node,
+        updateAccountsVisitor({
+            myAccount: {
+                data: { myData: 'myNewData' },
+            },
+        }),
+    );
+
+    // Then we expect the following tree changes.
+    assertIsNode(result, 'accountNode');
+    const data = resolveNestedTypeNode(result.data);
+    t.is(data.fields[0].name, 'myNewData' as CamelCaseString);
+});
+
+test('it updates the name of associated PDA nodes', t => {
+    // Given the following program node with one account
+    // and PDA accounts such that one of them is named the same.
+    const node = programNode({
+        accounts: [accountNode({ name: 'myAccount' })],
+        name: 'myProgram',
+        pdas: [pdaNode({ name: 'myAccount', seeds: [] }), pdaNode({ name: 'myOtherAccount', seeds: [] })],
+        publicKey: '1111',
+    });
+
+    // When we update the name of that account.
+    const result = visit(
+        node,
+        updateAccountsVisitor({
+            myAccount: { name: 'myNewAccount' },
+        }),
+    );
+
+    // Then we expect the associated PDA node to have been renamed.
+    assertIsNode(result, 'programNode');
+    t.is(result.pdas[0].name, 'myNewAccount' as CamelCaseString);
+
+    // But not the other PDA node.
+    t.is(result.pdas[1].name, 'myOtherAccount' as CamelCaseString);
+});
+
+test('it creates a new PDA node when providing seeds to an account with no linked PDA', t => {
+    // Given the following program node with one account.
+    const node = rootNode(
+        programNode({
+            accounts: [accountNode({ name: 'myAccount' })],
+            name: 'myProgramA',
+            pdas: [],
+            publicKey: '1111',
+        }),
+        [programNode({ name: 'myProgramB', publicKey: '2222' })],
+    );
+
+    // When we update the account with PDA seeds.
+    const seeds = [constantPdaSeedNodeFromString('utf8', 'myAccount')];
+    const result = visit(
+        node,
+        updateAccountsVisitor({
+            myAccount: { seeds },
+        }),
+    );
+    assertIsNode(result, 'rootNode');
+
+    // Then we expect a new PDA node to have been created on the program.
+    t.is(result.program.pdas.length, 1);
+    t.is(result.additionalPrograms[0].pdas.length, 0);
+    t.deepEqual(result.program.pdas[0], pdaNode({ name: 'myAccount', seeds }));
+
+    // And the account now links to the new PDA node.
+    t.deepEqual(result.program.accounts[0].pda, pdaLinkNode('myAccount'));
+});
+
+test('it updates the PDA node when the updated account name matches an existing PDA node', t => {
+    // Given an account node and a PDA node with the same name
+    // such that the account is not linked to the PDA.
+    const node = programNode({
+        accounts: [accountNode({ name: 'myAccount' })],
+        name: 'myProgram',
+        pdas: [pdaNode({ name: 'myAccount', seeds: [] })],
+        publicKey: '1111',
+    });
+
+    // When we update the account with PDA seeds.
+    const seeds = [constantPdaSeedNodeFromString('utf8', 'myAccount')];
+    const result = visit(
+        node,
+        updateAccountsVisitor({
+            myAccount: { seeds },
+        }),
+    );
+    assertIsNode(result, 'programNode');
+
+    // Then we expect the PDA node with the same name to have been updated.
+    t.is(result.pdas.length, 1);
+    t.deepEqual(result.pdas[0], pdaNode({ name: 'myAccount', seeds }));
+
+    // And the account now links to this PDA node.
+    t.deepEqual(result.accounts[0].pda, pdaLinkNode('myAccount'));
+});
+
+test('it updates the PDA node with the provided seeds when an account is linked to a PDA', t => {
+    // Given an account node and a PDA node with a different name
+    // such that the account is linked to the PDA.
+    const node = programNode({
+        accounts: [accountNode({ name: 'myAccount', pda: pdaLinkNode('myPda') })],
+        name: 'myProgram',
+        pdas: [pdaNode({ name: 'myPda', seeds: [] })],
+        publicKey: '1111',
+    });
+
+    // When we update the account with PDA seeds.
+    const seeds = [constantPdaSeedNodeFromString('utf8', 'myAccount')];
+    const result = visit(
+        node,
+        updateAccountsVisitor({
+            myAccount: { seeds },
+        }),
+    );
+    assertIsNode(result, 'programNode');
+
+    // Then we expect the linked PDA node to have been updated.
+    t.is(result.pdas.length, 1);
+    t.deepEqual(result.pdas[0], pdaNode({ name: 'myPda', seeds }));
+
+    // And the account still links to the PDA node.
+    t.deepEqual(result.accounts[0].pda, pdaLinkNode('myPda'));
+});
+
+test('it creates a new PDA node when updating an account with seeds and a new linked PDA that does not exist', t => {
+    // Given an account node with no linked PDA.
+    const node = programNode({
+        accounts: [accountNode({ name: 'myAccount' })],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // When we update the account with PDA seeds and a new linked PDA node.
+    const seeds = [constantPdaSeedNodeFromString('utf8', 'myAccount')];
+    const result = visit(
+        node,
+        updateAccountsVisitor({
+            myAccount: {
+                pda: pdaLinkNode('myPda'),
+                seeds,
+            },
+        }),
+    );
+    assertIsNode(result, 'programNode');
+
+    // Then we expect the linked PDA node to have been created.
+    t.is(result.pdas.length, 1);
+    t.deepEqual(result.pdas[0], pdaNode({ name: 'myPda', seeds }));
+
+    // And the account now links to the PDA node.
+    t.deepEqual(result.accounts[0].pda, pdaLinkNode('myPda'));
+});
+
+test('it updates a PDA node when updating an account with seeds and a new linked PDA that exists', t => {
+    // Given an account node with no linked PDA and an existing PDA node.
+    const node = programNode({
+        accounts: [accountNode({ name: 'myAccount' })],
+        name: 'myProgram',
+        pdas: [pdaNode({ name: 'myPda', seeds: [] })],
+        publicKey: '1111',
+    });
+
+    // When we update the account with PDA seeds and a linked PDA node that points to the existing PDA.
+    const seeds = [constantPdaSeedNodeFromString('utf8', 'myAccount')];
+    const result = visit(
+        node,
+        updateAccountsVisitor({
+            myAccount: {
+                pda: pdaLinkNode('myPda'),
+                seeds,
+            },
+        }),
+    );
+    assertIsNode(result, 'programNode');
+
+    // Then we expect the existing PDA node to have been updated.
+    t.is(result.pdas.length, 1);
+    t.deepEqual(result.pdas[0], pdaNode({ name: 'myPda', seeds }));
+
+    // And the account now links to this PDA node.
+    t.deepEqual(result.accounts[0].pda, pdaLinkNode('myPda'));
+});
+
+test('it can update the seeds and name of an account at the same time', t => {
+    // Given an account node with no linked PDA.
+    const node = programNode({
+        accounts: [accountNode({ name: 'myAccount' })],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // When we update the name and seeds of the account.
+    const seeds = [constantPdaSeedNodeFromString('utf8', 'myAccount')];
+    const result = visit(
+        node,
+        updateAccountsVisitor({
+            myAccount: {
+                name: 'myNewAccount',
+                seeds,
+            },
+        }),
+    );
+    assertIsNode(result, 'programNode');
+
+    // Then we expect the account name to have been updated.
+    t.is(result.accounts[0].name, 'myNewAccount' as CamelCaseString);
+
+    // And a new PDA node to have been created with that new name and the provided seeds.
+    t.is(result.pdas.length, 1);
+    t.deepEqual(result.pdas[0], pdaNode({ name: 'myNewAccount', seeds }));
+
+    // And the account to now link to the PDA node.
+    t.deepEqual(result.accounts[0].pda, pdaLinkNode('myNewAccount'));
+});

+ 201 - 0
packages/visitors/test/updateInstructionsVisitor.test.ts

@@ -0,0 +1,201 @@
+import {
+    assertIsNode,
+    CamelCaseString,
+    instructionAccountNode,
+    instructionArgumentNode,
+    instructionNode,
+    numberTypeNode,
+    numberValueNode,
+    programNode,
+    rootNode,
+} from '@kinobi-so/nodes';
+import { visit } from '@kinobi-so/visitors-core';
+import test from 'ava';
+
+import { updateInstructionsVisitor } from '../src/index.js';
+
+test('it updates the name of an instruction', t => {
+    // Given the following program node with one instruction.
+    const node = programNode({
+        instructions: [instructionNode({ name: 'myInstruction' })],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // When we update the name of that instruction.
+    const result = visit(
+        node,
+        updateInstructionsVisitor({
+            myInstruction: { name: 'myNewInstruction' },
+        }),
+    );
+
+    // Then we expect the following tree changes.
+    assertIsNode(result, 'programNode');
+    t.is(result.instructions[0].name, 'myNewInstruction' as CamelCaseString);
+});
+
+test('it updates the name of an instruction within a specific program', t => {
+    // Given two programs each with an instruction of the same name.
+    const node = rootNode(
+        programNode({
+            instructions: [instructionNode({ name: 'transfer' })],
+            name: 'myProgramA',
+            publicKey: '1111',
+        }),
+        [
+            programNode({
+                instructions: [instructionNode({ name: 'transfer' })],
+                name: 'myProgramB',
+                publicKey: '2222',
+            }),
+        ],
+    );
+
+    // When we update the name of that instruction in the first program.
+    const result = visit(
+        node,
+        updateInstructionsVisitor({
+            'myProgramA.transfer': { name: 'newTransfer' },
+        }),
+    );
+
+    // Then we expect the first instruction to have been renamed.
+    assertIsNode(result, 'rootNode');
+    t.is(result.program.instructions[0].name, 'newTransfer' as CamelCaseString);
+
+    // But not the second instruction.
+    t.is(result.additionalPrograms[0].instructions[0].name, 'transfer' as CamelCaseString);
+});
+
+test('it updates the name of an instruction account', t => {
+    // Given the following instruction node with one account.
+    const node = instructionNode({
+        accounts: [
+            instructionAccountNode({
+                isSigner: false,
+                isWritable: true,
+                name: 'myAccount',
+            }),
+        ],
+        name: 'myInstruction',
+    });
+
+    // When we update the name of that instruction account.
+    const result = visit(
+        node,
+        updateInstructionsVisitor({
+            myInstruction: {
+                accounts: {
+                    myAccount: { name: 'myNewAccount' },
+                },
+            },
+        }),
+    );
+
+    // Then we expect the following tree changes.
+    assertIsNode(result, 'instructionNode');
+    t.is(result.accounts[0].name, 'myNewAccount' as CamelCaseString);
+});
+
+test('it updates the name of an instruction argument', t => {
+    // Given the following instruction node with one argument.
+    const node = instructionNode({
+        arguments: [
+            instructionArgumentNode({
+                name: 'myArgument',
+                type: numberTypeNode('u8'),
+            }),
+        ],
+        name: 'myInstruction',
+    });
+
+    // When we update the name of that instruction argument.
+    const result = visit(
+        node,
+        updateInstructionsVisitor({
+            myInstruction: {
+                arguments: {
+                    myArgument: { name: 'myNewArgument' },
+                },
+            },
+        }),
+    );
+
+    // Then we expect the following tree changes.
+    assertIsNode(result, 'instructionNode');
+    t.is(result.arguments[0].name, 'myNewArgument' as CamelCaseString);
+});
+
+test('it updates the default value of an instruction argument', t => {
+    // Given the following instruction node with a argument that has no default value.
+    const node = instructionNode({
+        arguments: [
+            instructionArgumentNode({
+                name: 'amount',
+                type: numberTypeNode('u64'),
+            }),
+        ],
+        name: 'transferTokens',
+    });
+
+    // When we update the default value of that instruction argument.
+    const result = visit(
+        node,
+        updateInstructionsVisitor({
+            transferTokens: {
+                arguments: {
+                    amount: { defaultValue: numberValueNode(1) },
+                },
+            },
+        }),
+    );
+
+    // Then we expect the following tree changes.
+    assertIsNode(result, 'instructionNode');
+    t.deepEqual(result.arguments[0].defaultValue, numberValueNode(1));
+    t.is(result.arguments[0].defaultValueStrategy, undefined);
+});
+
+test('it updates the default value strategy of an instruction argument', t => {
+    // Given the following instruction node with two arguments that have no default values.
+    const node = instructionNode({
+        arguments: [
+            instructionArgumentNode({
+                name: 'discriminator',
+                type: numberTypeNode('u8'),
+            }),
+            instructionArgumentNode({
+                name: 'amount',
+                type: numberTypeNode('u64'),
+            }),
+        ],
+        name: 'transferTokens',
+    });
+
+    // When we update the default value of these arguments using specific strategies.
+    const result = visit(
+        node,
+        updateInstructionsVisitor({
+            transferTokens: {
+                arguments: {
+                    amount: {
+                        defaultValue: numberValueNode(1),
+                        defaultValueStrategy: 'optional',
+                    },
+                    discriminator: {
+                        defaultValue: numberValueNode(42),
+                        defaultValueStrategy: 'omitted',
+                    },
+                },
+            },
+        }),
+    );
+
+    // Then we expect the following tree changes.
+    assertIsNode(result, 'instructionNode');
+    t.deepEqual(result.arguments[0].defaultValue, numberValueNode(42));
+    t.is(result.arguments[0].defaultValueStrategy, 'omitted');
+    t.deepEqual(result.arguments[1].defaultValue, numberValueNode(1));
+    t.is(result.arguments[1].defaultValueStrategy, 'optional');
+});

+ 10 - 0
packages/visitors/tsconfig.declarations.json

@@ -0,0 +1,10 @@
+{
+    "compilerOptions": {
+        "declaration": true,
+        "declarationMap": true,
+        "emitDeclarationOnly": true,
+        "outDir": "./dist/types"
+    },
+    "extends": "./tsconfig.json",
+    "include": ["src/index.ts", "src/types"]
+}

+ 7 - 0
packages/visitors/tsconfig.json

@@ -0,0 +1,7 @@
+{
+    "$schema": "https://json.schemastore.org/tsconfig",
+    "compilerOptions": { "lib": [] },
+    "display": "@kinobi-so/visitors",
+    "extends": "../internals/tsconfig.base.json",
+    "include": ["src", "test"]
+}

+ 9 - 0
pnpm-lock.yaml

@@ -98,6 +98,15 @@ importers:
         specifier: workspace:*
         version: link:../node-types
 
+  packages/visitors:
+    dependencies:
+      '@kinobi-so/nodes':
+        specifier: workspace:*
+        version: link:../nodes
+      '@kinobi-so/visitors-core':
+        specifier: workspace:*
+        version: link:../visitors-core
+
   packages/visitors-core:
     dependencies:
       '@kinobi-so/nodes':