Pārlūkot izejas kodu

[0.22] Add `InstructionLinkNode`, `InstructionAccountLinkNode` and `InstructionArgumentLinkNode` (#183)

Co-Authored-By: Danny Povolotski <etodanik@users.noreply.github.com>
Loris Leiva 1 gadu atpakaļ
vecāks
revīzija
c8c5934662
42 mainītis faili ar 1018 papildinājumiem un 163 dzēšanām
  1. 11 0
      .changeset/hungry-hairs-shave.md
  2. 1 1
      packages/errors/src/context.ts
  3. 14 0
      packages/node-types/src/linkNodes/InstructionAccountLinkNode.ts
  4. 14 0
      packages/node-types/src/linkNodes/InstructionArgumentLinkNode.ts
  5. 12 0
      packages/node-types/src/linkNodes/InstructionLinkNode.ts
  6. 11 1
      packages/node-types/src/linkNodes/LinkNode.ts
  7. 3 0
      packages/node-types/src/linkNodes/index.ts
  8. 3 0
      packages/nodes/README.md
  9. 1 1
      packages/nodes/docs/linkNodes/AccountLinkNode.md
  10. 38 0
      packages/nodes/docs/linkNodes/InstructionAccountLinkNode.md
  11. 38 0
      packages/nodes/docs/linkNodes/InstructionArgumentLinkNode.md
  12. 29 0
      packages/nodes/docs/linkNodes/InstructionLinkNode.md
  13. 3 0
      packages/nodes/docs/linkNodes/README.md
  14. 21 0
      packages/nodes/src/linkNodes/InstructionAccountLinkNode.ts
  15. 21 0
      packages/nodes/src/linkNodes/InstructionArgumentLinkNode.ts
  16. 16 0
      packages/nodes/src/linkNodes/InstructionLinkNode.ts
  17. 5 2
      packages/nodes/src/linkNodes/LinkNode.ts
  18. 3 0
      packages/nodes/src/linkNodes/index.ts
  19. 13 0
      packages/nodes/test/linkNodes/InstructionAccountLinkNode.test.ts
  20. 13 0
      packages/nodes/test/linkNodes/InstructionArgumentLinkNode.test.ts
  21. 13 0
      packages/nodes/test/linkNodes/InstructionLinkNode.test.ts
  22. 13 13
      packages/renderers-js-umi/README.md
  23. 2 2
      packages/renderers-js-umi/src/getRenderMapVisitor.ts
  24. 30 4
      packages/renderers-js-umi/src/utils/linkOverrides.ts
  25. 15 15
      packages/renderers-js/README.md
  26. 30 4
      packages/renderers-js/src/utils/linkOverrides.ts
  27. 9 9
      packages/renderers-rust/README.md
  28. 1 0
      packages/renderers-rust/src/ImportMap.ts
  29. 30 4
      packages/renderers-rust/src/utils/linkOverrides.ts
  30. 4 2
      packages/visitors-core/README.md
  31. 146 67
      packages/visitors-core/src/LinkableDictionary.ts
  32. 5 1
      packages/visitors-core/src/NodeStack.ts
  33. 6 4
      packages/visitors-core/src/getDebugStringVisitor.ts
  34. 54 0
      packages/visitors-core/src/identityVisitor.ts
  35. 36 0
      packages/visitors-core/src/mergeVisitor.ts
  36. 8 2
      packages/visitors-core/test/nodes/linkNodes/AccountLinkNode.test.ts
  37. 8 2
      packages/visitors-core/test/nodes/linkNodes/DefinedTypeLinkNode.test.ts
  38. 35 0
      packages/visitors-core/test/nodes/linkNodes/InstructionAccountLinkNode.test.ts
  39. 35 0
      packages/visitors-core/test/nodes/linkNodes/InstructionArgumentLinkNode.test.ts
  40. 33 0
      packages/visitors-core/test/nodes/linkNodes/InstructionLinkNode.test.ts
  41. 8 2
      packages/visitors-core/test/nodes/linkNodes/PdaLinkNode.test.ts
  42. 227 27
      packages/visitors-core/test/recordLinkablesVisitor.test.ts

+ 11 - 0
.changeset/hungry-hairs-shave.md

@@ -0,0 +1,11 @@
+---
+'@kinobi-so/renderers-js-umi': minor
+'@kinobi-so/renderers-rust': minor
+'@kinobi-so/visitors-core': minor
+'@kinobi-so/renderers-js': minor
+'@kinobi-so/node-types': minor
+'@kinobi-so/errors': minor
+'@kinobi-so/nodes': minor
+---
+
+Add `InstructionLinkNode`, `InstructionAccountLinkNode` and `InstructionArgumentLinkNode`

+ 1 - 1
packages/errors/src/context.ts

@@ -75,7 +75,7 @@ export type KinobiErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
         kind: LinkNode['kind'];
         linkNode: LinkNode;
         name: CamelCaseString;
-        program?: CamelCaseString;
+        stack: Node[];
     };
     [KINOBI_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE]: {
         fsFunction: string;

+ 14 - 0
packages/node-types/src/linkNodes/InstructionAccountLinkNode.ts

@@ -0,0 +1,14 @@
+import type { CamelCaseString } from '../shared';
+import type { InstructionLinkNode } from './InstructionLinkNode';
+
+export interface InstructionAccountLinkNode<
+    TInstruction extends InstructionLinkNode | undefined = InstructionLinkNode | undefined,
+> {
+    readonly kind: 'instructionAccountLinkNode';
+
+    // Children.
+    readonly instruction?: TInstruction;
+
+    // Data.
+    readonly name: CamelCaseString;
+}

+ 14 - 0
packages/node-types/src/linkNodes/InstructionArgumentLinkNode.ts

@@ -0,0 +1,14 @@
+import type { CamelCaseString } from '../shared';
+import type { InstructionLinkNode } from './InstructionLinkNode';
+
+export interface InstructionArgumentLinkNode<
+    TInstruction extends InstructionLinkNode | undefined = InstructionLinkNode | undefined,
+> {
+    readonly kind: 'instructionArgumentLinkNode';
+
+    // Children.
+    readonly instruction?: TInstruction;
+
+    // Data.
+    readonly name: CamelCaseString;
+}

+ 12 - 0
packages/node-types/src/linkNodes/InstructionLinkNode.ts

@@ -0,0 +1,12 @@
+import type { CamelCaseString } from '../shared';
+import type { ProgramLinkNode } from './ProgramLinkNode';
+
+export interface InstructionLinkNode<TProgram extends ProgramLinkNode | undefined = ProgramLinkNode | undefined> {
+    readonly kind: 'instructionLinkNode';
+
+    // Children.
+    readonly program?: TProgram;
+
+    // Data.
+    readonly name: CamelCaseString;
+}

+ 11 - 1
packages/node-types/src/linkNodes/LinkNode.ts

@@ -1,10 +1,20 @@
 import type { AccountLinkNode } from './AccountLinkNode';
 import type { DefinedTypeLinkNode } from './DefinedTypeLinkNode';
+import type { InstructionAccountLinkNode } from './InstructionAccountLinkNode';
+import type { InstructionArgumentLinkNode } from './InstructionArgumentLinkNode';
+import type { InstructionLinkNode } from './InstructionLinkNode';
 import type { PdaLinkNode } from './PdaLinkNode';
 import type { ProgramLinkNode } from './ProgramLinkNode';
 
 // Link Node Registration.
-export type RegisteredLinkNode = AccountLinkNode | DefinedTypeLinkNode | PdaLinkNode | ProgramLinkNode;
+export type RegisteredLinkNode =
+    | AccountLinkNode
+    | DefinedTypeLinkNode
+    | InstructionAccountLinkNode
+    | InstructionArgumentLinkNode
+    | InstructionLinkNode
+    | PdaLinkNode
+    | ProgramLinkNode;
 
 // Link Node Helpers.
 export type LinkNode = RegisteredLinkNode;

+ 3 - 0
packages/node-types/src/linkNodes/index.ts

@@ -1,5 +1,8 @@
 export * from './AccountLinkNode';
 export * from './DefinedTypeLinkNode';
+export * from './InstructionAccountLinkNode';
+export * from './InstructionArgumentLinkNode';
+export * from './InstructionLinkNode';
 export * from './LinkNode';
 export * from './PdaLinkNode';
 export * from './ProgramLinkNode';

+ 3 - 0
packages/nodes/README.md

@@ -61,6 +61,9 @@ Below are all of the available nodes and their documentation. Also note that you
 -   [`LinkNode`](./docs/linkNodes/README.md) (abstract)
     -   [`AccountLinkNode`](./docs/linkNodes/AccountLinkNode.md)
     -   [`DefinedTypeLinkNode`](./docs/linkNodes/DefinedTypeLinkNode.md)
+    -   [`InstructionAccountLinkNode`](./docs/linkNodes/InstructionAccountLinkNode.md)
+    -   [`InstructionArgumentLinkNode`](./docs/linkNodes/InstructionArgumentLinkNode.md)
+    -   [`InstructionLinkNode`](./docs/linkNodes/InstructionLinkNode.md)
     -   [`PdaLinkNode`](./docs/linkNodes/PdaLinkNode.md)
     -   [`ProgramLinkNode`](./docs/linkNodes/ProgramLinkNode.md)
 -   [`PdaSeedNode`](./docs/pdaSeedNodes/README.md) (abstract)

+ 1 - 1
packages/nodes/docs/linkNodes/AccountLinkNode.md

@@ -21,7 +21,7 @@ This node represents a reference to an existing [`AccountNode`](../AccountNode.m
 
 ### `accountLinkNode(name, program?)`
 
-Helper function that creates a `AccountLinkNode` object from the name of the `AccountNode` we are referring to. If the account is from another program, the `program` parameter must be provided as either a `string` or a `ProgramLinkNode`.
+Helper function that creates an `AccountLinkNode` object from the name of the `AccountNode` we are referring to. If the account is from another program, the `program` parameter must be provided as either a `string` or a `ProgramLinkNode`.
 
 ```ts
 const node = accountLinkNode('myAccount');

+ 38 - 0
packages/nodes/docs/linkNodes/InstructionAccountLinkNode.md

@@ -0,0 +1,38 @@
+# `InstructionAccountLinkNode`
+
+This node represents a reference to an existing [`InstructionAccountNode`](../InstructionAccountNode.md) in the Kinobi IDL.
+
+## Attributes
+
+### Data
+
+| Attribute | Type                           | Description                                                                                   |
+| --------- | ------------------------------ | --------------------------------------------------------------------------------------------- |
+| `kind`    | `"instructionAccountLinkNode"` | The node discriminator.                                                                       |
+| `name`    | `CamelCaseString`              | The name of the [`InstructionAccountNode`](../InstructionAccountNode.md) we are referring to. |
+
+### Children
+
+| Attribute     | Type                                              | Description                                                                                                                                                                                          |
+| ------------- | ------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `instruction` | [`InstructionLinkNode`](./InstructionLinkNode.md) | (Optional) The instruction associated with the linked account. Default to using the instruction we are currently under. Note that the instruction itself can point to a different program is needed. |
+
+## Functions
+
+### `instructionAccountLinkNode(name, instruction?)`
+
+Helper function that creates an `InstructionAccountLinkNode` object from the name of the `InstructionAccountNode` we are referring to. If the account is from another instruction, the `instruction` parameter must be provided as either a `string` or a `InstructionLinkNode`. When providing an `InstructionLinkNode`, we can also provide a `ProgramLinkNode` to point to a different program.
+
+```ts
+// Links to an account in the current instruction.
+const node = instructionAccountLinkNode('myAccount');
+
+// Links to an account in another instruction but within the same program.
+const nodeFromAnotherInstruction = instructionAccountLinkNode('myAccount', 'myOtherInstruction');
+
+// Links to an account in another instruction from another program.
+const nodeFromAnotherProgram = instructionAccountLinkNode(
+    'myAccount',
+    instructionLinkNode('myOtherInstruction', 'myOtherProgram'),
+);
+```

+ 38 - 0
packages/nodes/docs/linkNodes/InstructionArgumentLinkNode.md

@@ -0,0 +1,38 @@
+# `InstructionArgumentLinkNode`
+
+This node represents a reference to an existing [`InstructionArgumentNode`](../InstructionArgumentNode.md) in the Kinobi IDL.
+
+## Attributes
+
+### Data
+
+| Attribute | Type                            | Description                                                                                     |
+| --------- | ------------------------------- | ----------------------------------------------------------------------------------------------- |
+| `kind`    | `"instructionArgumentLinkNode"` | The node discriminator.                                                                         |
+| `name`    | `CamelCaseString`               | The name of the [`InstructionArgumentNode`](../InstructionArgumentNode.md) we are referring to. |
+
+### Children
+
+| Attribute     | Type                                              | Description                                                                                                                                                                                           |
+| ------------- | ------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `instruction` | [`InstructionLinkNode`](./InstructionLinkNode.md) | (Optional) The instruction associated with the linked argument. Default to using the instruction we are currently under. Note that the instruction itself can point to a different program is needed. |
+
+## Functions
+
+### `instructionArgumentLinkNode(name, instruction?)`
+
+Helper function that creates an `InstructionArgumentLinkNode` object from the name of the `InstructionArgumentNode` we are referring to. If the argument is from another instruction, the `instruction` parameter must be provided as either a `string` or a `InstructionLinkNode`. When providing an `InstructionLinkNode`, we can also provide a `ProgramLinkNode` to point to a different program.
+
+```ts
+// Links to an argument in the current instruction.
+const node = instructionArgumentLinkNode('myArgument');
+
+// Links to an argument in another instruction but within the same program.
+const nodeFromAnotherInstruction = instructionArgumentLinkNode('myArgument', 'myOtherInstruction');
+
+// Links to an argument in another instruction from another program.
+const nodeFromAnotherProgram = instructionArgumentLinkNode(
+    'myArgument',
+    instructionLinkNode('myOtherInstruction', 'myOtherProgram'),
+);
+```

+ 29 - 0
packages/nodes/docs/linkNodes/InstructionLinkNode.md

@@ -0,0 +1,29 @@
+# `InstructionLinkNode`
+
+This node represents a reference to an existing [`InstructionNode`](../InstructionNode.md) in the Kinobi IDL.
+
+## Attributes
+
+### Data
+
+| Attribute | Type                    | Description                                                                     |
+| --------- | ----------------------- | ------------------------------------------------------------------------------- |
+| `kind`    | `"instructionLinkNode"` | The node discriminator.                                                         |
+| `name`    | `CamelCaseString`       | The name of the [`InstructionNode`](../InstructionNode.md) we are referring to. |
+
+### Children
+
+| Attribute | Type                                      | Description                                                                                                         |
+| --------- | ----------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
+| `program` | [`ProgramLinkNode`](./ProgramLinkNode.md) | (Optional) The program associated with the linked instruction. Default to using the program we are currently under. |
+
+## Functions
+
+### `instructionLinkNode(name, program?)`
+
+Helper function that creates an `InstructionLinkNode` object from the name of the `InstructionNode` we are referring to. If the instruction is from another program, the `program` parameter must be provided as either a `string` or a `ProgramLinkNode`.
+
+```ts
+const node = instructionLinkNode('myInstruction');
+const nodeFromAnotherProgram = instructionLinkNode('myInstruction', 'myOtherProgram');
+```

+ 3 - 0
packages/nodes/docs/linkNodes/README.md

@@ -4,5 +4,8 @@ The `LinkNode` type helper represents all nodes that link to other nodes. Note t
 
 -   [`AccountLinkNode`](./AccountLinkNode.md)
 -   [`DefinedTypeLinkNode`](./DefinedTypeLinkNode.md)
+-   [`InstructionAccountLinkNode`](./InstructionAccountLinkNode.md)
+-   [`InstructionArgumentLinkNode`](./InstructionArgumentLinkNode.md)
+-   [`InstructionLinkNode`](./InstructionLinkNode.md)
 -   [`PdaLinkNode`](./PdaLinkNode.md)
 -   [`ProgramLinkNode`](./ProgramLinkNode.md)

+ 21 - 0
packages/nodes/src/linkNodes/InstructionAccountLinkNode.ts

@@ -0,0 +1,21 @@
+import type { InstructionAccountLinkNode, InstructionLinkNode } from '@kinobi-so/node-types';
+
+import { camelCase } from '../shared';
+import { instructionLinkNode } from './InstructionLinkNode';
+
+export function instructionAccountLinkNode(
+    name: string,
+    instruction?: InstructionLinkNode | string,
+): InstructionAccountLinkNode {
+    return Object.freeze({
+        kind: 'instructionAccountLinkNode',
+
+        // Children.
+        ...(instruction === undefined
+            ? {}
+            : { instruction: typeof instruction === 'string' ? instructionLinkNode(instruction) : instruction }),
+
+        // Data.
+        name: camelCase(name),
+    });
+}

+ 21 - 0
packages/nodes/src/linkNodes/InstructionArgumentLinkNode.ts

@@ -0,0 +1,21 @@
+import type { InstructionArgumentLinkNode, InstructionLinkNode } from '@kinobi-so/node-types';
+
+import { camelCase } from '../shared';
+import { instructionLinkNode } from './InstructionLinkNode';
+
+export function instructionArgumentLinkNode(
+    name: string,
+    instruction?: InstructionLinkNode | string,
+): InstructionArgumentLinkNode {
+    return Object.freeze({
+        kind: 'instructionArgumentLinkNode',
+
+        // Children.
+        ...(instruction === undefined
+            ? {}
+            : { instruction: typeof instruction === 'string' ? instructionLinkNode(instruction) : instruction }),
+
+        // Data.
+        name: camelCase(name),
+    });
+}

+ 16 - 0
packages/nodes/src/linkNodes/InstructionLinkNode.ts

@@ -0,0 +1,16 @@
+import type { InstructionLinkNode, ProgramLinkNode } from '@kinobi-so/node-types';
+
+import { camelCase } from '../shared';
+import { programLinkNode } from './ProgramLinkNode';
+
+export function instructionLinkNode(name: string, program?: ProgramLinkNode | string): InstructionLinkNode {
+    return Object.freeze({
+        kind: 'instructionLinkNode',
+
+        // Children.
+        ...(program === undefined ? {} : { program: typeof program === 'string' ? programLinkNode(program) : program }),
+
+        // Data.
+        name: camelCase(name),
+    });
+}

+ 5 - 2
packages/nodes/src/linkNodes/LinkNode.ts

@@ -1,9 +1,12 @@
 // Link Node Registration.
 export const REGISTERED_LINK_NODE_KINDS = [
-    'programLinkNode' as const,
-    'pdaLinkNode' as const,
     'accountLinkNode' as const,
     'definedTypeLinkNode' as const,
+    'instructionAccountLinkNode' as const,
+    'instructionArgumentLinkNode' as const,
+    'instructionLinkNode' as const,
+    'pdaLinkNode' as const,
+    'programLinkNode' as const,
 ];
 
 // Link Node Helpers.

+ 3 - 0
packages/nodes/src/linkNodes/index.ts

@@ -1,5 +1,8 @@
 export * from './AccountLinkNode';
 export * from './DefinedTypeLinkNode';
+export * from './InstructionAccountLinkNode';
+export * from './InstructionArgumentLinkNode';
+export * from './InstructionLinkNode';
 export * from './LinkNode';
 export * from './PdaLinkNode';
 export * from './ProgramLinkNode';

+ 13 - 0
packages/nodes/test/linkNodes/InstructionAccountLinkNode.test.ts

@@ -0,0 +1,13 @@
+import { expect, test } from 'vitest';
+
+import { instructionAccountLinkNode } from '../../src';
+
+test('it returns the right node kind', () => {
+    const node = instructionAccountLinkNode('mint');
+    expect(node.kind).toBe('instructionAccountLinkNode');
+});
+
+test('it returns a frozen object', () => {
+    const node = instructionAccountLinkNode('mint');
+    expect(Object.isFrozen(node)).toBe(true);
+});

+ 13 - 0
packages/nodes/test/linkNodes/InstructionArgumentLinkNode.test.ts

@@ -0,0 +1,13 @@
+import { expect, test } from 'vitest';
+
+import { instructionArgumentLinkNode } from '../../src';
+
+test('it returns the right node kind', () => {
+    const node = instructionArgumentLinkNode('amount');
+    expect(node.kind).toBe('instructionArgumentLinkNode');
+});
+
+test('it returns a frozen object', () => {
+    const node = instructionArgumentLinkNode('amount');
+    expect(Object.isFrozen(node)).toBe(true);
+});

+ 13 - 0
packages/nodes/test/linkNodes/InstructionLinkNode.test.ts

@@ -0,0 +1,13 @@
+import { expect, test } from 'vitest';
+
+import { instructionLinkNode } from '../../src';
+
+test('it returns the right node kind', () => {
+    const node = instructionLinkNode('transferTokens');
+    expect(node.kind).toBe('instructionLinkNode');
+});
+
+test('it returns a frozen object', () => {
+    const node = instructionLinkNode('transferTokens');
+    expect(Object.isFrozen(node)).toBe(true);
+});

+ 13 - 13
packages/renderers-js-umi/README.md

@@ -37,16 +37,16 @@ kinobi.accept(renderVisitor(pathToGeneratedFolder, options));
 
 The `renderVisitor` accepts the following options.
 
-| Name                          | Type                                                                                                  | Default   | Description                                                                                                                                                                                      |
-| ----------------------------- | ----------------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `deleteFolderBeforeRendering` | `boolean`                                                                                             | `true`    | Whether the base directory should be cleaned before generating new files.                                                                                                                        |
-| `formatCode`                  | `boolean`                                                                                             | `true`    | Whether we should use Prettier to format the generated code.                                                                                                                                     |
-| `prettierOptions`             | `PrettierOptions`                                                                                     | `{}`      | The options to use when formatting the code using Prettier.                                                                                                                                      |
-| `throwLevel`                  | `'debug' \| 'trace' \| 'info' \| 'warn' \| 'error'`                                                   | `'error'` | When validating the Kinobi IDL, the level at which the validation should throw an error.                                                                                                         |
-| `customAccountData`           | `string[]`                                                                                            | `[]`      | The names of all `AccountNodes` whose data should be manually written in JavaScript.                                                                                                             |
-| `customInstructionData`       | `string[]`                                                                                            | `[]`      | The names of all `InstructionNodes` whose data should be manually written in JavaScript.                                                                                                         |
-| `linkOverrides`               | `Record<'accounts' \| 'definedTypes' \| 'pdas' \| 'programs' \| 'resolvers', Record<string, string>>` | `{}`      | A object that overrides the import path of link nodes. For instance, `{ definedTypes: { counter: 'hooked' } }` uses the `hooked` folder to import any link node referring to the `counter` type. |
-| `dependencyMap`               | `Record<string, string>`                                                                              | `{}`      | A mapping between import aliases and their actual package name or path in JavaScript.                                                                                                            |
-| `internalNodes`               | `string[]`                                                                                            | `[]`      | The names of all nodes that should be generated but not exported by the `index.ts` files.                                                                                                        |
-| `nonScalarEnums`              | `string[]`                                                                                            | `[]`      | The names of enum variants with no data that should be treated as a data union instead of a native `enum` type. This is only useful if you are referencing an enum value in your Kinobi IDL.     |
-| `renderParentInstructions`    | `boolean`                                                                                             | `false`   | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                          |
+| Name                          | Type                                                                                                                    | Default   | Description                                                                                                                                                                                      |
+| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | --------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `deleteFolderBeforeRendering` | `boolean`                                                                                                               | `true`    | Whether the base directory should be cleaned before generating new files.                                                                                                                        |
+| `formatCode`                  | `boolean`                                                                                                               | `true`    | Whether we should use Prettier to format the generated code.                                                                                                                                     |
+| `prettierOptions`             | `PrettierOptions`                                                                                                       | `{}`      | The options to use when formatting the code using Prettier.                                                                                                                                      |
+| `throwLevel`                  | `'debug' \| 'trace' \| 'info' \| 'warn' \| 'error'`                                                                     | `'error'` | When validating the Kinobi IDL, the level at which the validation should throw an error.                                                                                                         |
+| `customAccountData`           | `string[]`                                                                                                              | `[]`      | The names of all `AccountNodes` whose data should be manually written in JavaScript.                                                                                                             |
+| `customInstructionData`       | `string[]`                                                                                                              | `[]`      | The names of all `InstructionNodes` whose data should be manually written in JavaScript.                                                                                                         |
+| `linkOverrides`               | `Record<'accounts' \| 'definedTypes' \| 'instructions' \| 'pdas' \| 'programs' \| 'resolvers', Record<string, string>>` | `{}`      | A object that overrides the import path of link nodes. For instance, `{ definedTypes: { counter: 'hooked' } }` uses the `hooked` folder to import any link node referring to the `counter` type. |
+| `dependencyMap`               | `Record<string, string>`                                                                                                | `{}`      | A mapping between import aliases and their actual package name or path in JavaScript.                                                                                                            |
+| `internalNodes`               | `string[]`                                                                                                              | `[]`      | The names of all nodes that should be generated but not exported by the `index.ts` files.                                                                                                        |
+| `nonScalarEnums`              | `string[]`                                                                                                              | `[]`      | The names of enum variants with no data that should be treated as a data union instead of a native `enum` type. This is only useful if you are referencing an enum value in your Kinobi IDL.     |
+| `renderParentInstructions`    | `boolean`                                                                                                               | `false`   | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                          |

+ 2 - 2
packages/renderers-js-umi/src/getRenderMapVisitor.ts

@@ -73,10 +73,10 @@ export function getRenderMapVisitor(options: GetRenderMapOptions = {}): Visitor<
         umiSerializers: '@metaplex-foundation/umi/serializers',
         ...options.dependencyMap,
 
+        // Custom relative dependencies to link generated files together.
         generatedAccounts: '../accounts',
         generatedErrors: '../errors',
-
-        // Custom relative dependencies to link generated files together.
+        generatedInstructions: '../instructions',
         generatedPrograms: '../programs',
         generatedTypes: '../types',
     };

+ 30 - 4
packages/renderers-js-umi/src/utils/linkOverrides.ts

@@ -1,17 +1,33 @@
 import { KINOBI_ERROR__UNEXPECTED_NODE_KIND, KinobiError } from '@kinobi-so/errors';
-import { LINK_NODES, LinkNode, ResolverValueNode } from '@kinobi-so/nodes';
+import {
+    AccountLinkNode,
+    DefinedTypeLinkNode,
+    InstructionLinkNode,
+    PdaLinkNode,
+    ProgramLinkNode,
+    ResolverValueNode,
+} from '@kinobi-so/nodes';
 
 import { ParsedCustomDataOptions } from './customData';
 
 export type LinkOverrides = {
     accounts?: Record<string, string>;
     definedTypes?: Record<string, string>;
+    instructions?: Record<string, string>;
     pdas?: Record<string, string>;
     programs?: Record<string, string>;
     resolvers?: Record<string, string>;
 };
 
-export type GetImportFromFunction = (node: LinkNode | ResolverValueNode, fallback?: string) => string;
+type OverridableNodes =
+    | AccountLinkNode
+    | DefinedTypeLinkNode
+    | InstructionLinkNode
+    | PdaLinkNode
+    | ProgramLinkNode
+    | ResolverValueNode;
+
+export type GetImportFromFunction = (node: OverridableNodes, fallback?: string) => string;
 
 export function getImportFromFactory(
     overrides: LinkOverrides,
@@ -27,18 +43,21 @@ export function getImportFromFactory(
     const linkOverrides = {
         accounts: overrides.accounts ?? {},
         definedTypes: { ...customDataOverrides, ...overrides.definedTypes },
+        instructions: overrides.instructions ?? {},
         pdas: overrides.pdas ?? {},
         programs: overrides.programs ?? {},
         resolvers: overrides.resolvers ?? {},
     };
 
-    return (node: LinkNode | ResolverValueNode) => {
+    return (node: OverridableNodes) => {
         const kind = node.kind;
         switch (kind) {
             case 'accountLinkNode':
                 return linkOverrides.accounts[node.name] ?? 'generatedAccounts';
             case 'definedTypeLinkNode':
                 return linkOverrides.definedTypes[node.name] ?? 'generatedTypes';
+            case 'instructionLinkNode':
+                return linkOverrides.instructions[node.name] ?? 'generatedInstructions';
             case 'pdaLinkNode':
                 return linkOverrides.pdas[node.name] ?? 'generatedAccounts';
             case 'programLinkNode':
@@ -47,7 +66,14 @@ export function getImportFromFactory(
                 return linkOverrides.resolvers[node.name] ?? 'hooked';
             default:
                 throw new KinobiError(KINOBI_ERROR__UNEXPECTED_NODE_KIND, {
-                    expectedKinds: [...LINK_NODES, 'resolverValueNode'],
+                    expectedKinds: [
+                        'AccountLinkNode',
+                        'DefinedTypeLinkNode',
+                        'InstructionLinkNode',
+                        'PdaLinkNode',
+                        'ProgramLinkNode',
+                        'resolverValueNode',
+                    ],
                     kind: kind satisfies never,
                     node,
                 });

+ 15 - 15
packages/renderers-js/README.md

@@ -37,18 +37,18 @@ kinobi.accept(renderVisitor(pathToGeneratedFolder, options));
 
 The `renderVisitor` accepts the following options.
 
-| Name                          | Type                                                                                                  | Default | Description                                                                                                                                                                                                                                                     |
-| ----------------------------- | ----------------------------------------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `deleteFolderBeforeRendering` | `boolean`                                                                                             | `true`  | Whether the base directory should be cleaned before generating new files.                                                                                                                                                                                       |
-| `formatCode`                  | `boolean`                                                                                             | `true`  | Whether we should use Prettier to format the generated code.                                                                                                                                                                                                    |
-| `prettierOptions`             | `PrettierOptions`                                                                                     | `{}`    | The options to use when formatting the code using Prettier.                                                                                                                                                                                                     |
-| `asyncResolvers`              | `string[]`                                                                                            | `[]`    | The exhaustive list of `ResolverValueNode`'s names whose implementation is asynchronous in JavaScript.                                                                                                                                                          |
-| `customAccountData`           | `string[]`                                                                                            | `[]`    | The names of all `AccountNodes` whose data should be manually written in JavaScript.                                                                                                                                                                            |
-| `customInstructionData`       | `string[]`                                                                                            | `[]`    | The names of all `InstructionNodes` whose data should be manually written in JavaScript.                                                                                                                                                                        |
-| `linkOverrides`               | `Record<'accounts' \| 'definedTypes' \| 'pdas' \| 'programs' \| 'resolvers', Record<string, string>>` | `{}`    | A object that overrides the import path of link nodes. For instance, `{ definedTypes: { counter: 'hooked' } }` uses the `hooked` folder to import any link node referring to the `counter` type.                                                                |
-| `dependencyMap`               | `Record<string, string>`                                                                              | `{}`    | A mapping between import aliases and their actual package name or path in JavaScript.                                                                                                                                                                           |
-| `internalNodes`               | `string[]`                                                                                            | `[]`    | The names of all nodes that should be generated but not exported by the `index.ts` files.                                                                                                                                                                       |
-| `nameTransformers`            | `Partial<NameTransformers>`                                                                           | `{}`    | An object that enables us to override the names of any generated type, constant or function.                                                                                                                                                                    |
-| `nonScalarEnums`              | `string[]`                                                                                            | `[]`    | The names of enum variants with no data that should be treated as a data union instead of a native `enum` type. This is only useful if you are referencing an enum value in your Kinobi IDL.                                                                    |
-| `renderParentInstructions`    | `boolean`                                                                                             | `false` | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                                                                                         |
-| `useGranularImports`          | `boolean`                                                                                             | `false` | Whether to import the `@solana/web3.js` library using sub-packages such as `@solana/addresses` or `@solana/codecs-strings`. When set to `true`, the main `@solana/web3.js` library is used which enables generated clients to install it as a `peerDependency`. |
+| Name                          | Type                                                                                                                    | Default | Description                                                                                                                                                                                                                                                     |
+| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `deleteFolderBeforeRendering` | `boolean`                                                                                                               | `true`  | Whether the base directory should be cleaned before generating new files.                                                                                                                                                                                       |
+| `formatCode`                  | `boolean`                                                                                                               | `true`  | Whether we should use Prettier to format the generated code.                                                                                                                                                                                                    |
+| `prettierOptions`             | `PrettierOptions`                                                                                                       | `{}`    | The options to use when formatting the code using Prettier.                                                                                                                                                                                                     |
+| `asyncResolvers`              | `string[]`                                                                                                              | `[]`    | The exhaustive list of `ResolverValueNode`'s names whose implementation is asynchronous in JavaScript.                                                                                                                                                          |
+| `customAccountData`           | `string[]`                                                                                                              | `[]`    | The names of all `AccountNodes` whose data should be manually written in JavaScript.                                                                                                                                                                            |
+| `customInstructionData`       | `string[]`                                                                                                              | `[]`    | The names of all `InstructionNodes` whose data should be manually written in JavaScript.                                                                                                                                                                        |
+| `linkOverrides`               | `Record<'accounts' \| 'definedTypes' \| 'instructions' \| 'pdas' \| 'programs' \| 'resolvers', Record<string, string>>` | `{}`    | A object that overrides the import path of link nodes. For instance, `{ definedTypes: { counter: 'hooked' } }` uses the `hooked` folder to import any link node referring to the `counter` type.                                                                |
+| `dependencyMap`               | `Record<string, string>`                                                                                                | `{}`    | A mapping between import aliases and their actual package name or path in JavaScript.                                                                                                                                                                           |
+| `internalNodes`               | `string[]`                                                                                                              | `[]`    | The names of all nodes that should be generated but not exported by the `index.ts` files.                                                                                                                                                                       |
+| `nameTransformers`            | `Partial<NameTransformers>`                                                                                             | `{}`    | An object that enables us to override the names of any generated type, constant or function.                                                                                                                                                                    |
+| `nonScalarEnums`              | `string[]`                                                                                                              | `[]`    | The names of enum variants with no data that should be treated as a data union instead of a native `enum` type. This is only useful if you are referencing an enum value in your Kinobi IDL.                                                                    |
+| `renderParentInstructions`    | `boolean`                                                                                                               | `false` | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                                                                                         |
+| `useGranularImports`          | `boolean`                                                                                                               | `false` | Whether to import the `@solana/web3.js` library using sub-packages such as `@solana/addresses` or `@solana/codecs-strings`. When set to `true`, the main `@solana/web3.js` library is used which enables generated clients to install it as a `peerDependency`. |

+ 30 - 4
packages/renderers-js/src/utils/linkOverrides.ts

@@ -1,17 +1,33 @@
 import { KINOBI_ERROR__UNEXPECTED_NODE_KIND, KinobiError } from '@kinobi-so/errors';
-import { LINK_NODES, LinkNode, ResolverValueNode } from '@kinobi-so/nodes';
+import {
+    AccountLinkNode,
+    DefinedTypeLinkNode,
+    InstructionLinkNode,
+    PdaLinkNode,
+    ProgramLinkNode,
+    ResolverValueNode,
+} from '@kinobi-so/nodes';
 
 import { ParsedCustomDataOptions } from './customData';
 
 export type LinkOverrides = {
     accounts?: Record<string, string>;
     definedTypes?: Record<string, string>;
+    instructions?: Record<string, string>;
     pdas?: Record<string, string>;
     programs?: Record<string, string>;
     resolvers?: Record<string, string>;
 };
 
-export type GetImportFromFunction = (node: LinkNode | ResolverValueNode, fallback?: string) => string;
+type OverridableNodes =
+    | AccountLinkNode
+    | DefinedTypeLinkNode
+    | InstructionLinkNode
+    | PdaLinkNode
+    | ProgramLinkNode
+    | ResolverValueNode;
+
+export type GetImportFromFunction = (node: OverridableNodes, fallback?: string) => string;
 
 export function getImportFromFactory(
     overrides: LinkOverrides,
@@ -27,18 +43,21 @@ export function getImportFromFactory(
     const linkOverrides = {
         accounts: overrides.accounts ?? {},
         definedTypes: { ...customDataOverrides, ...overrides.definedTypes },
+        instructions: overrides.instructions ?? {},
         pdas: overrides.pdas ?? {},
         programs: overrides.programs ?? {},
         resolvers: overrides.resolvers ?? {},
     };
 
-    return (node: LinkNode | ResolverValueNode) => {
+    return (node: OverridableNodes) => {
         const kind = node.kind;
         switch (kind) {
             case 'accountLinkNode':
                 return linkOverrides.accounts[node.name] ?? 'generatedAccounts';
             case 'definedTypeLinkNode':
                 return linkOverrides.definedTypes[node.name] ?? 'generatedTypes';
+            case 'instructionLinkNode':
+                return linkOverrides.instructions[node.name] ?? 'generatedInstructions';
             case 'pdaLinkNode':
                 return linkOverrides.pdas[node.name] ?? 'generatedPdas';
             case 'programLinkNode':
@@ -47,7 +66,14 @@ export function getImportFromFactory(
                 return linkOverrides.resolvers[node.name] ?? 'hooked';
             default:
                 throw new KinobiError(KINOBI_ERROR__UNEXPECTED_NODE_KIND, {
-                    expectedKinds: [...LINK_NODES, 'resolverValueNode'],
+                    expectedKinds: [
+                        'AccountLinkNode',
+                        'DefinedTypeLinkNode',
+                        'InstructionLinkNode',
+                        'PdaLinkNode',
+                        'ProgramLinkNode',
+                        'resolverValueNode',
+                    ],
                     kind: kind satisfies never,
                     node,
                 });

+ 9 - 9
packages/renderers-rust/README.md

@@ -37,12 +37,12 @@ kinobi.accept(renderVisitor(pathToGeneratedFolder, options));
 
 The `renderVisitor` accepts the following options.
 
-| Name                          | Type                                                                                                  | Default     | Description                                                                                                                                                                                      |
-| ----------------------------- | ----------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
-| `deleteFolderBeforeRendering` | `boolean`                                                                                             | `true`      | Whether the base directory should be cleaned before generating new files.                                                                                                                        |
-| `formatCode`                  | `boolean`                                                                                             | `false`     | Whether we should use `cargo fmt` to format the generated code. When set to `true`, the `crateFolder` option must be provided.                                                                   |
-| `toolchain`                   | `string`                                                                                              | `"+stable"` | The toolchain to use when formatting the generated code.                                                                                                                                         |
-| `crateFolder`                 | `string`                                                                                              | none        | The path to the root folder of the Rust crate. This option is required when `formatCode` is set to `true`.                                                                                       |
-| `linkOverrides`               | `Record<'accounts' \| 'definedTypes' \| 'pdas' \| 'programs' \| 'resolvers', Record<string, string>>` | `{}`        | A object that overrides the import path of link nodes. For instance, `{ definedTypes: { counter: 'hooked' } }` uses the `hooked` folder to import any link node referring to the `counter` type. |
-| `dependencyMap`               | `Record<string, string>`                                                                              | `{}`        | A mapping between import aliases and their actual crate name or path in Rust.                                                                                                                    |
-| `renderParentInstructions`    | `boolean`                                                                                             | `false`     | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                          |
+| Name                          | Type                                                                                                                    | Default     | Description                                                                                                                                                                                      |
+| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `deleteFolderBeforeRendering` | `boolean`                                                                                                               | `true`      | Whether the base directory should be cleaned before generating new files.                                                                                                                        |
+| `formatCode`                  | `boolean`                                                                                                               | `false`     | Whether we should use `cargo fmt` to format the generated code. When set to `true`, the `crateFolder` option must be provided.                                                                   |
+| `toolchain`                   | `string`                                                                                                                | `"+stable"` | The toolchain to use when formatting the generated code.                                                                                                                                         |
+| `crateFolder`                 | `string`                                                                                                                | none        | The path to the root folder of the Rust crate. This option is required when `formatCode` is set to `true`.                                                                                       |
+| `linkOverrides`               | `Record<'accounts' \| 'definedTypes' \| 'instructions' \| 'pdas' \| 'programs' \| 'resolvers', Record<string, string>>` | `{}`        | A object that overrides the import path of link nodes. For instance, `{ definedTypes: { counter: 'hooked' } }` uses the `hooked` folder to import any link node referring to the `counter` type. |
+| `dependencyMap`               | `Record<string, string>`                                                                                                | `{}`        | A mapping between import aliases and their actual crate name or path in Rust.                                                                                                                    |
+| `renderParentInstructions`    | `boolean`                                                                                                               | `false`     | When using nested instructions, whether the parent instructions should also be rendered. When set to `false` (default), only the instruction leaves are being rendered.                          |

+ 1 - 0
packages/renderers-rust/src/ImportMap.ts

@@ -4,6 +4,7 @@ const DEFAULT_MODULE_MAP: Record<string, string> = {
     generated: 'crate::generated',
     generatedAccounts: 'crate::generated::accounts',
     generatedErrors: 'crate::generated::errors',
+    generatedInstructions: 'crate::generated::instructions',
     generatedTypes: 'crate::generated::types',
     hooked: 'crate::hooked',
     mplEssentials: 'mpl_toolbox',

+ 30 - 4
packages/renderers-rust/src/utils/linkOverrides.ts

@@ -1,32 +1,51 @@
 import { KINOBI_ERROR__UNEXPECTED_NODE_KIND, KinobiError } from '@kinobi-so/errors';
-import { LINK_NODES, LinkNode, ResolverValueNode } from '@kinobi-so/nodes';
+import {
+    AccountLinkNode,
+    DefinedTypeLinkNode,
+    InstructionLinkNode,
+    PdaLinkNode,
+    ProgramLinkNode,
+    ResolverValueNode,
+} from '@kinobi-so/nodes';
 
 export type LinkOverrides = {
     accounts?: Record<string, string>;
     definedTypes?: Record<string, string>;
+    instructions?: Record<string, string>;
     pdas?: Record<string, string>;
     programs?: Record<string, string>;
     resolvers?: Record<string, string>;
 };
 
-export type GetImportFromFunction = (node: LinkNode | ResolverValueNode, fallback?: string) => string;
+type OverridableNodes =
+    | AccountLinkNode
+    | DefinedTypeLinkNode
+    | InstructionLinkNode
+    | PdaLinkNode
+    | ProgramLinkNode
+    | ResolverValueNode;
+
+export type GetImportFromFunction = (node: OverridableNodes, fallback?: string) => string;
 
 export function getImportFromFactory(overrides: LinkOverrides): GetImportFromFunction {
     const linkOverrides = {
         accounts: overrides.accounts ?? {},
         definedTypes: overrides.definedTypes ?? {},
+        instructions: overrides.instructions ?? {},
         pdas: overrides.pdas ?? {},
         programs: overrides.programs ?? {},
         resolvers: overrides.resolvers ?? {},
     };
 
-    return (node: LinkNode | ResolverValueNode) => {
+    return (node: OverridableNodes) => {
         const kind = node.kind;
         switch (kind) {
             case 'accountLinkNode':
                 return linkOverrides.accounts[node.name] ?? 'generatedAccounts';
             case 'definedTypeLinkNode':
                 return linkOverrides.definedTypes[node.name] ?? 'generatedTypes';
+            case 'instructionLinkNode':
+                return linkOverrides.instructions[node.name] ?? 'generatedInstructions';
             case 'pdaLinkNode':
                 return linkOverrides.pdas[node.name] ?? 'generatedAccounts';
             case 'programLinkNode':
@@ -35,7 +54,14 @@ export function getImportFromFactory(overrides: LinkOverrides): GetImportFromFun
                 return linkOverrides.resolvers[node.name] ?? 'hooked';
             default:
                 throw new KinobiError(KINOBI_ERROR__UNEXPECTED_NODE_KIND, {
-                    expectedKinds: [...LINK_NODES, 'resolverValueNode'],
+                    expectedKinds: [
+                        'AccountLinkNode',
+                        'DefinedTypeLinkNode',
+                        'InstructionLinkNode',
+                        'PdaLinkNode',
+                        'ProgramLinkNode',
+                        'resolverValueNode',
+                    ],
                     kind: kind satisfies never,
                     node,
                 });

+ 4 - 2
packages/visitors-core/README.md

@@ -443,10 +443,12 @@ const lastNode = nodeStack.pop();
 const lastNode = nodeStack.peek();
 // Get all the nodes in the stack as an array.
 const nodes = nodeStack.all();
-// Get the first node in the stack matching one or several node kinds.
+// Get the closest node in the stack matching one or several node kinds.
 const nodes = nodeStack.find('accountNode');
-// Get the first program node in the stack.
+// Get the closest program node in the stack.
 const nodes = nodeStack.getProgram();
+// Get the closest instruction node in the stack.
+const nodes = nodeStack.getInstruction();
 // Check if the stack is empty.
 const isEmpty = nodeStack.isEmpty();
 // Clone the stack.

+ 146 - 67
packages/visitors-core/src/LinkableDictionary.ts

@@ -2,10 +2,15 @@ import { KINOBI_ERROR__LINKED_NODE_NOT_FOUND, KinobiError } from '@kinobi-so/err
 import {
     AccountLinkNode,
     AccountNode,
-    camelCase,
     CamelCaseString,
     DefinedTypeLinkNode,
     DefinedTypeNode,
+    InstructionAccountLinkNode,
+    InstructionAccountNode,
+    InstructionArgumentLinkNode,
+    InstructionArgumentNode,
+    InstructionLinkNode,
+    InstructionNode,
     isNode,
     LinkNode,
     PdaLinkNode,
@@ -16,70 +21,71 @@ import {
 
 import { NodeStack } from './NodeStack';
 
-export type LinkableNode = AccountNode | DefinedTypeNode | PdaNode | ProgramNode;
-
-export const LINKABLE_NODES: LinkableNode['kind'][] = ['accountNode', 'definedTypeNode', 'pdaNode', 'programNode'];
+export type LinkableNode =
+    | AccountNode
+    | DefinedTypeNode
+    | InstructionAccountNode
+    | InstructionArgumentNode
+    | InstructionNode
+    | PdaNode
+    | ProgramNode;
+
+export const LINKABLE_NODES: LinkableNode['kind'][] = [
+    'accountNode',
+    'definedTypeNode',
+    'instructionAccountNode',
+    'instructionArgumentNode',
+    'instructionNode',
+    'pdaNode',
+    'programNode',
+];
 
 type ProgramDictionary = {
     accounts: Map<string, AccountNode>;
     definedTypes: Map<string, DefinedTypeNode>;
+    instructions: Map<string, InstructionDictionary>;
     pdas: Map<string, PdaNode>;
     program: ProgramNode;
 };
 
-type ProgramInput = ProgramLinkNode | ProgramNode | string;
-
-function getProgramName(program: ProgramInput): CamelCaseString;
-function getProgramName(program: ProgramInput | undefined): CamelCaseString | undefined;
-function getProgramName(program: ProgramInput | undefined): CamelCaseString | undefined {
-    if (!program) return undefined;
-    return typeof program === 'string' ? camelCase(program) : program.name;
-}
+type InstructionDictionary = {
+    accounts: Map<string, InstructionAccountNode>;
+    arguments: Map<string, InstructionArgumentNode>;
+    instruction: InstructionNode;
+};
 
 export class LinkableDictionary {
     readonly programs: Map<string, ProgramDictionary> = new Map();
 
     readonly stack: NodeStack = new NodeStack();
 
-    private getOrCreateProgramDictionary(node: ProgramNode): ProgramDictionary {
-        let programDictionary = this.programs.get(node.name);
-        if (!programDictionary) {
-            programDictionary = {
-                accounts: new Map(),
-                definedTypes: new Map(),
-                pdas: new Map(),
-                program: node,
-            };
-            this.programs.set(node.name, programDictionary);
-        }
-        return programDictionary;
-    }
-
     record(node: LinkableNode): this {
-        if (isNode(node, 'programNode')) {
-            this.getOrCreateProgramDictionary(node);
-            return this;
-        }
+        const programDictionary = this.getOrCreateProgramDictionary(node);
+        if (!programDictionary) return this; // Do not record nodes that are outside of a program.
+        const instructionDictionary = this.getOrCreateInstructionDictionary(programDictionary, node);
 
-        // Do not record nodes that are outside of a program.
-        const program = this.stack.getProgram();
-        if (!program) return this;
-
-        const programDictionary = this.getOrCreateProgramDictionary(program);
-        if (isNode(node, 'pdaNode')) {
-            programDictionary.pdas.set(node.name, node);
-        } else if (isNode(node, 'accountNode')) {
+        if (isNode(node, 'accountNode')) {
             programDictionary.accounts.set(node.name, node);
         } else if (isNode(node, 'definedTypeNode')) {
             programDictionary.definedTypes.set(node.name, node);
+        } else if (isNode(node, 'pdaNode')) {
+            programDictionary.pdas.set(node.name, node);
+        } else if (instructionDictionary && isNode(node, 'instructionAccountNode')) {
+            instructionDictionary.accounts.set(node.name, node);
+        } else if (instructionDictionary && isNode(node, 'instructionArgumentNode')) {
+            instructionDictionary.arguments.set(node.name, node);
         }
+
         return this;
     }
 
-    getOrThrow(linkNode: ProgramLinkNode): ProgramNode;
-    getOrThrow(linkNode: PdaLinkNode): PdaNode;
     getOrThrow(linkNode: AccountLinkNode): AccountNode;
     getOrThrow(linkNode: DefinedTypeLinkNode): DefinedTypeNode;
+    getOrThrow(linkNode: InstructionAccountLinkNode): InstructionAccountNode;
+    getOrThrow(linkNode: InstructionArgumentLinkNode): InstructionArgumentNode;
+    getOrThrow(linkNode: InstructionLinkNode): InstructionNode;
+    getOrThrow(linkNode: PdaLinkNode): PdaNode;
+    getOrThrow(linkNode: ProgramLinkNode): ProgramNode;
     getOrThrow(linkNode: LinkNode): LinkableNode {
         const node = this.get(linkNode as ProgramLinkNode) as LinkableNode | undefined;
 
@@ -88,60 +94,133 @@ export class LinkableDictionary {
                 kind: linkNode.kind,
                 linkNode,
                 name: linkNode.name,
-                program: isNode(linkNode, 'pdaLinkNode')
-                    ? getProgramName(linkNode.program ?? this.stack.getProgram())
-                    : undefined,
+                stack: this.stack.all(),
             });
         }
 
         return node;
     }
 
-    get(linkNode: ProgramLinkNode): ProgramNode | undefined;
-    get(linkNode: PdaLinkNode): PdaNode | undefined;
     get(linkNode: AccountLinkNode): AccountNode | undefined;
     get(linkNode: DefinedTypeLinkNode): DefinedTypeNode | undefined;
+    get(linkNode: InstructionAccountLinkNode): InstructionAccountNode | undefined;
+    get(linkNode: InstructionArgumentLinkNode): InstructionArgumentNode | undefined;
+    get(linkNode: InstructionLinkNode): InstructionNode | undefined;
+    get(linkNode: PdaLinkNode): PdaNode | undefined;
+    get(linkNode: ProgramLinkNode): ProgramNode | undefined;
     get(linkNode: LinkNode): LinkableNode | undefined {
-        if (isNode(linkNode, 'programLinkNode')) {
-            return this.programs.get(linkNode.name)?.program;
-        }
-
-        const programName = getProgramName(linkNode.program ?? this.stack.getProgram());
-        if (!programName) return undefined;
-
-        const programDictionary = this.programs.get(programName);
+        const programDictionary = this.getProgramDictionary(linkNode);
         if (!programDictionary) return undefined;
+        const instructionDictionary = this.getInstructionDictionary(programDictionary, linkNode);
 
-        if (isNode(linkNode, 'pdaLinkNode')) {
-            return programDictionary.pdas.get(linkNode.name);
-        } else if (isNode(linkNode, 'accountLinkNode')) {
+        if (isNode(linkNode, 'accountLinkNode')) {
             return programDictionary.accounts.get(linkNode.name);
         } else if (isNode(linkNode, 'definedTypeLinkNode')) {
             return programDictionary.definedTypes.get(linkNode.name);
+        } else if (isNode(linkNode, 'instructionAccountLinkNode')) {
+            return instructionDictionary?.accounts.get(linkNode.name);
+        } else if (isNode(linkNode, 'instructionArgumentLinkNode')) {
+            return instructionDictionary?.arguments.get(linkNode.name);
+        } else if (isNode(linkNode, 'instructionLinkNode')) {
+            return instructionDictionary?.instruction;
+        } else if (isNode(linkNode, 'pdaLinkNode')) {
+            return programDictionary.pdas.get(linkNode.name);
+        } else if (isNode(linkNode, 'programLinkNode')) {
+            return programDictionary.program;
         }
 
         return undefined;
     }
 
     has(linkNode: LinkNode): boolean {
-        if (isNode(linkNode, 'programLinkNode')) {
-            return this.programs.has(linkNode.name);
-        }
-
-        const programName = getProgramName(linkNode.program ?? this.stack.getProgram());
-        if (!programName) return false;
-
-        const programDictionary = this.programs.get(programName);
+        const programDictionary = this.getProgramDictionary(linkNode);
         if (!programDictionary) return false;
+        const instructionDictionary = this.getInstructionDictionary(programDictionary, linkNode);
 
-        if (isNode(linkNode, 'pdaLinkNode')) {
-            return programDictionary.pdas.has(linkNode.name);
-        } else if (isNode(linkNode, 'accountLinkNode')) {
+        if (isNode(linkNode, 'accountLinkNode')) {
             return programDictionary.accounts.has(linkNode.name);
         } else if (isNode(linkNode, 'definedTypeLinkNode')) {
             return programDictionary.definedTypes.has(linkNode.name);
+        } else if (isNode(linkNode, 'instructionAccountLinkNode')) {
+            return !!instructionDictionary && instructionDictionary.accounts.has(linkNode.name);
+        } else if (isNode(linkNode, 'instructionArgumentLinkNode')) {
+            return !!instructionDictionary && instructionDictionary.arguments.has(linkNode.name);
+        } else if (isNode(linkNode, 'instructionLinkNode')) {
+            return programDictionary.instructions.has(linkNode.name);
+        } else if (isNode(linkNode, 'pdaLinkNode')) {
+            return programDictionary.pdas.has(linkNode.name);
+        } else if (isNode(linkNode, 'programLinkNode')) {
+            return true;
         }
 
         return false;
     }
+
+    private getOrCreateProgramDictionary(node: LinkableNode): ProgramDictionary | undefined {
+        const programNode = isNode(node, 'programNode') ? node : this.stack.getProgram();
+        if (!programNode) return undefined;
+
+        let programDictionary = this.programs.get(programNode.name);
+        if (!programDictionary) {
+            programDictionary = {
+                accounts: new Map(),
+                definedTypes: new Map(),
+                instructions: new Map(),
+                pdas: new Map(),
+                program: programNode,
+            };
+            this.programs.set(programNode.name, programDictionary);
+        }
+
+        return programDictionary;
+    }
+
+    private getOrCreateInstructionDictionary(
+        programDictionary: ProgramDictionary,
+        node: LinkableNode,
+    ): InstructionDictionary | undefined {
+        const instructionNode = isNode(node, 'instructionNode') ? node : this.stack.getInstruction();
+        if (!instructionNode) return undefined;
+
+        let instructionDictionary = programDictionary.instructions.get(instructionNode.name);
+        if (!instructionDictionary) {
+            instructionDictionary = {
+                accounts: new Map(),
+                arguments: new Map(),
+                instruction: instructionNode,
+            };
+            programDictionary.instructions.set(instructionNode.name, instructionDictionary);
+        }
+
+        return instructionDictionary;
+    }
+
+    private getProgramDictionary(linkNode: LinkNode): ProgramDictionary | undefined {
+        let programName: CamelCaseString | undefined = undefined;
+        if (isNode(linkNode, 'programLinkNode')) {
+            programName = linkNode.name;
+        } else if ('program' in linkNode) {
+            programName = linkNode.program?.name;
+        } else if ('instruction' in linkNode) {
+            programName = linkNode.instruction?.program?.name;
+        }
+        programName = programName ?? this.stack.getProgram()?.name;
+
+        return programName ? this.programs.get(programName) : undefined;
+    }
+
+    private getInstructionDictionary(
+        programDictionary: ProgramDictionary,
+        linkNode: LinkNode,
+    ): InstructionDictionary | undefined {
+        let instructionName: CamelCaseString | undefined = undefined;
+        if (isNode(linkNode, 'instructionLinkNode')) {
+            instructionName = linkNode.name;
+        } else if ('instruction' in linkNode) {
+            instructionName = linkNode.instruction?.name;
+        }
+        instructionName = instructionName ?? this.stack.getInstruction()?.name;
+
+        return instructionName ? programDictionary.instructions.get(instructionName) : undefined;
+    }
 }

+ 5 - 1
packages/visitors-core/src/NodeStack.ts

@@ -1,4 +1,4 @@
-import { GetNodeFromKind, isNode, Node, NodeKind, ProgramNode } from '@kinobi-so/nodes';
+import { GetNodeFromKind, InstructionNode, isNode, Node, NodeKind, ProgramNode } from '@kinobi-so/nodes';
 
 export class NodeStack {
     private readonly stack: Node[];
@@ -31,6 +31,10 @@ export class NodeStack {
         return this.find('programNode');
     }
 
+    public getInstruction(): InstructionNode | undefined {
+        return this.find('instructionNode');
+    }
+
     public all(): readonly Node[] {
         return [...this.stack];
     }

+ 6 - 4
packages/visitors-core/src/getDebugStringVisitor.ts

@@ -63,12 +63,14 @@ function getNodeDetails(node: Node): string[] {
             return [...(node.subtract ? ['subtract'] : []), ...(node.withHeader ? ['withHeader'] : [])];
         case 'errorNode':
             return [node.code.toString(), node.name];
-        case 'programLinkNode':
-            return [node.name];
-        case 'pdaLinkNode':
         case 'accountLinkNode':
         case 'definedTypeLinkNode':
-            return [...(node.program ? [node.program.name] : []), node.name];
+        case 'instructionAccountLinkNode':
+        case 'instructionArgumentLinkNode':
+        case 'instructionLinkNode':
+        case 'pdaLinkNode':
+        case 'programLinkNode':
+            return [node.name];
         case 'numberTypeNode':
             return [node.format, ...(node.endian === 'be' ? ['bigEndian'] : [])];
         case 'amountTypeNode':

+ 54 - 0
packages/visitors-core/src/identityVisitor.ts

@@ -1,4 +1,5 @@
 import {
+    accountLinkNode,
     accountNode,
     amountTypeNode,
     arrayTypeNode,
@@ -12,6 +13,7 @@ import {
     constantValueNode,
     COUNT_NODES,
     dateTimeTypeNode,
+    definedTypeLinkNode,
     definedTypeNode,
     DISCRIMINATOR_NODES,
     ENUM_VARIANT_TYPE_NODES,
@@ -24,9 +26,12 @@ import {
     hiddenPrefixTypeNode,
     hiddenSuffixTypeNode,
     INSTRUCTION_INPUT_VALUE_NODES,
+    instructionAccountLinkNode,
     instructionAccountNode,
+    instructionArgumentLinkNode,
     instructionArgumentNode,
     instructionByteDeltaNode,
+    instructionLinkNode,
     instructionNode,
     instructionRemainingAccountsNode,
     mapEntryValueNode,
@@ -36,6 +41,7 @@ import {
     NodeKind,
     optionTypeNode,
     PDA_SEED_NODES,
+    pdaLinkNode,
     pdaNode,
     pdaSeedValueNode,
     pdaValueNode,
@@ -624,5 +630,53 @@ export function identityVisitor<TNodeKind extends NodeKind = NodeKind>(
         };
     }
 
+    if (castedNodeKeys.includes('accountLinkNode')) {
+        visitor.visitAccountLink = function visitAccountLink(node) {
+            const program = node.program ? (visit(this)(node.program) ?? undefined) : undefined;
+            if (program) assertIsNode(program, 'programLinkNode');
+            return accountLinkNode(node.name, program);
+        };
+    }
+
+    if (castedNodeKeys.includes('definedTypeLinkNode')) {
+        visitor.visitDefinedTypeLink = function visitDefinedTypeLink(node) {
+            const program = node.program ? (visit(this)(node.program) ?? undefined) : undefined;
+            if (program) assertIsNode(program, 'programLinkNode');
+            return definedTypeLinkNode(node.name, program);
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionLinkNode')) {
+        visitor.visitInstructionLink = function visitInstructionLink(node) {
+            const program = node.program ? (visit(this)(node.program) ?? undefined) : undefined;
+            if (program) assertIsNode(program, 'programLinkNode');
+            return instructionLinkNode(node.name, program);
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionAccountLinkNode')) {
+        visitor.visitInstructionAccountLink = function visitInstructionAccountLink(node) {
+            const instruction = node.instruction ? (visit(this)(node.instruction) ?? undefined) : undefined;
+            if (instruction) assertIsNode(instruction, 'instructionLinkNode');
+            return instructionAccountLinkNode(node.name, instruction);
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionArgumentLinkNode')) {
+        visitor.visitInstructionArgumentLink = function visitInstructionArgumentLink(node) {
+            const instruction = node.instruction ? (visit(this)(node.instruction) ?? undefined) : undefined;
+            if (instruction) assertIsNode(instruction, 'instructionLinkNode');
+            return instructionArgumentLinkNode(node.name, instruction);
+        };
+    }
+
+    if (castedNodeKeys.includes('pdaLinkNode')) {
+        visitor.visitPdaLink = function visitPdaLink(node) {
+            const program = node.program ? (visit(this)(node.program) ?? undefined) : undefined;
+            if (program) assertIsNode(program, 'programLinkNode');
+            return pdaLinkNode(node.name, program);
+        };
+    }
+
     return visitor as Visitor<Node, TNodeKind>;
 }

+ 36 - 0
packages/visitors-core/src/mergeVisitor.ts

@@ -350,5 +350,41 @@ export function mergeVisitor<TReturn, TNodeKind extends NodeKind = NodeKind>(
         };
     }
 
+    if (castedNodeKeys.includes('accountLinkNode')) {
+        visitor.visitAccountLink = function visitAccountLink(node) {
+            return merge(node, node.program ? visit(this)(node.program) : []);
+        };
+    }
+
+    if (castedNodeKeys.includes('definedTypeLinkNode')) {
+        visitor.visitDefinedTypeLink = function visitDefinedTypeLink(node) {
+            return merge(node, node.program ? visit(this)(node.program) : []);
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionLinkNode')) {
+        visitor.visitInstructionLink = function visitInstructionLink(node) {
+            return merge(node, node.program ? visit(this)(node.program) : []);
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionAccountLinkNode')) {
+        visitor.visitInstructionAccountLink = function visitInstructionAccountLink(node) {
+            return merge(node, node.instruction ? visit(this)(node.instruction) : []);
+        };
+    }
+
+    if (castedNodeKeys.includes('instructionArgumentLinkNode')) {
+        visitor.visitInstructionArgumentLink = function visitInstructionArgumentLink(node) {
+            return merge(node, node.instruction ? visit(this)(node.instruction) : []);
+        };
+    }
+
+    if (castedNodeKeys.includes('pdaLinkNode')) {
+        visitor.visitPdaLink = function visitPdaLink(node) {
+            return merge(node, node.program ? visit(this)(node.program) : []);
+        };
+    }
+
     return visitor as Visitor<TReturn, TNodeKind>;
 }

+ 8 - 2
packages/visitors-core/test/nodes/linkNodes/AccountLinkNode.test.ts

@@ -11,7 +11,7 @@ import {
 const node = accountLinkNode('token', 'splToken');
 
 test('mergeVisitor', () => {
-    expectMergeVisitorCount(node, 1);
+    expectMergeVisitorCount(node, 2);
 });
 
 test('identityVisitor', () => {
@@ -20,8 +20,14 @@ test('identityVisitor', () => {
 
 test('deleteNodesVisitor', () => {
     expectDeleteNodesVisitor(node, '[accountLinkNode]', null);
+    expectDeleteNodesVisitor(node, '[programLinkNode]', accountLinkNode('token'));
 });
 
 test('debugStringVisitor', () => {
-    expectDebugStringVisitor(node, `accountLinkNode [splToken.token]`);
+    expectDebugStringVisitor(
+        node,
+        `
+accountLinkNode [token]
+|   programLinkNode [splToken]`,
+    );
 });

+ 8 - 2
packages/visitors-core/test/nodes/linkNodes/DefinedTypeLinkNode.test.ts

@@ -11,7 +11,7 @@ import {
 const node = definedTypeLinkNode('tokenState', 'splToken');
 
 test('mergeVisitor', () => {
-    expectMergeVisitorCount(node, 1);
+    expectMergeVisitorCount(node, 2);
 });
 
 test('identityVisitor', () => {
@@ -20,8 +20,14 @@ test('identityVisitor', () => {
 
 test('deleteNodesVisitor', () => {
     expectDeleteNodesVisitor(node, '[definedTypeLinkNode]', null);
+    expectDeleteNodesVisitor(node, '[programLinkNode]', definedTypeLinkNode('tokenState'));
 });
 
 test('debugStringVisitor', () => {
-    expectDebugStringVisitor(node, `definedTypeLinkNode [splToken.tokenState]`);
+    expectDebugStringVisitor(
+        node,
+        `
+definedTypeLinkNode [tokenState]
+|   programLinkNode [splToken]`,
+    );
 });

+ 35 - 0
packages/visitors-core/test/nodes/linkNodes/InstructionAccountLinkNode.test.ts

@@ -0,0 +1,35 @@
+import { instructionAccountLinkNode, instructionLinkNode } from '@kinobi-so/nodes';
+import { test } from 'vitest';
+
+import {
+    expectDebugStringVisitor,
+    expectDeleteNodesVisitor,
+    expectIdentityVisitor,
+    expectMergeVisitorCount,
+} from '../_setup';
+
+const node = instructionAccountLinkNode('mint', instructionLinkNode('transferTokens', 'splToken'));
+
+test('mergeVisitor', () => {
+    expectMergeVisitorCount(node, 3);
+});
+
+test('identityVisitor', () => {
+    expectIdentityVisitor(node);
+});
+
+test('deleteNodesVisitor', () => {
+    expectDeleteNodesVisitor(node, '[instructionAccountLinkNode]', null);
+    expectDeleteNodesVisitor(node, '[instructionLinkNode]', instructionAccountLinkNode('mint'));
+    expectDeleteNodesVisitor(node, '[programLinkNode]', instructionAccountLinkNode('mint', 'transferTokens'));
+});
+
+test('debugStringVisitor', () => {
+    expectDebugStringVisitor(
+        node,
+        `
+instructionAccountLinkNode [mint]
+|   instructionLinkNode [transferTokens]
+|   |   programLinkNode [splToken]`,
+    );
+});

+ 35 - 0
packages/visitors-core/test/nodes/linkNodes/InstructionArgumentLinkNode.test.ts

@@ -0,0 +1,35 @@
+import { instructionArgumentLinkNode, instructionLinkNode } from '@kinobi-so/nodes';
+import { test } from 'vitest';
+
+import {
+    expectDebugStringVisitor,
+    expectDeleteNodesVisitor,
+    expectIdentityVisitor,
+    expectMergeVisitorCount,
+} from '../_setup';
+
+const node = instructionArgumentLinkNode('amount', instructionLinkNode('transferTokens', 'splToken'));
+
+test('mergeVisitor', () => {
+    expectMergeVisitorCount(node, 3);
+});
+
+test('identityVisitor', () => {
+    expectIdentityVisitor(node);
+});
+
+test('deleteNodesVisitor', () => {
+    expectDeleteNodesVisitor(node, '[instructionArgumentLinkNode]', null);
+    expectDeleteNodesVisitor(node, '[instructionLinkNode]', instructionArgumentLinkNode('amount'));
+    expectDeleteNodesVisitor(node, '[programLinkNode]', instructionArgumentLinkNode('amount', 'transferTokens'));
+});
+
+test('debugStringVisitor', () => {
+    expectDebugStringVisitor(
+        node,
+        `
+instructionArgumentLinkNode [amount]
+|   instructionLinkNode [transferTokens]
+|   |   programLinkNode [splToken]`,
+    );
+});

+ 33 - 0
packages/visitors-core/test/nodes/linkNodes/InstructionLinkNode.test.ts

@@ -0,0 +1,33 @@
+import { instructionLinkNode } from '@kinobi-so/nodes';
+import { test } from 'vitest';
+
+import {
+    expectDebugStringVisitor,
+    expectDeleteNodesVisitor,
+    expectIdentityVisitor,
+    expectMergeVisitorCount,
+} from '../_setup';
+
+const node = instructionLinkNode('transferTokens', 'splToken');
+
+test('mergeVisitor', () => {
+    expectMergeVisitorCount(node, 2);
+});
+
+test('identityVisitor', () => {
+    expectIdentityVisitor(node);
+});
+
+test('deleteNodesVisitor', () => {
+    expectDeleteNodesVisitor(node, '[instructionLinkNode]', null);
+    expectDeleteNodesVisitor(node, '[programLinkNode]', instructionLinkNode('transferTokens'));
+});
+
+test('debugStringVisitor', () => {
+    expectDebugStringVisitor(
+        node,
+        `
+instructionLinkNode [transferTokens]
+|   programLinkNode [splToken]`,
+    );
+});

+ 8 - 2
packages/visitors-core/test/nodes/linkNodes/PdaLinkNode.test.ts

@@ -11,7 +11,7 @@ import {
 const node = pdaLinkNode('associatedToken', 'splToken');
 
 test('mergeVisitor', () => {
-    expectMergeVisitorCount(node, 1);
+    expectMergeVisitorCount(node, 2);
 });
 
 test('identityVisitor', () => {
@@ -20,8 +20,14 @@ test('identityVisitor', () => {
 
 test('deleteNodesVisitor', () => {
     expectDeleteNodesVisitor(node, '[pdaLinkNode]', null);
+    expectDeleteNodesVisitor(node, '[programLinkNode]', pdaLinkNode('associatedToken'));
 });
 
 test('debugStringVisitor', () => {
-    expectDebugStringVisitor(node, `pdaLinkNode [splToken.associatedToken]`);
+    expectDebugStringVisitor(
+        node,
+        `
+pdaLinkNode [associatedToken]
+|   programLinkNode [splToken]`,
+    );
 });

+ 227 - 27
packages/visitors-core/test/recordLinkablesVisitor.test.ts

@@ -1,16 +1,24 @@
+import { KINOBI_ERROR__LINKED_NODE_NOT_FOUND, KinobiError } from '@kinobi-so/errors';
 import {
     accountLinkNode,
     AccountNode,
     accountNode,
     definedTypeLinkNode,
     definedTypeNode,
+    instructionAccountLinkNode,
+    InstructionAccountNode,
+    instructionAccountNode,
+    instructionArgumentLinkNode,
+    instructionArgumentNode,
+    instructionLinkNode,
+    instructionNode,
     isNode,
+    numberTypeNode,
     pdaLinkNode,
     pdaNode,
     programLinkNode,
     programNode,
     rootNode,
-    structTypeNode,
 } from '@kinobi-so/nodes';
 import { expect, test } from 'vitest';
 
@@ -23,26 +31,155 @@ import {
     voidVisitor,
 } from '../src';
 
-test('it record all linkable nodes it finds when traversing the tree', () => {
-    // Given the following root node containing multiple linkable nodes.
-    const node = rootNode(
-        programNode({
-            accounts: [accountNode({ name: 'accountA' })],
-            definedTypes: [definedTypeNode({ name: 'typeA', type: structTypeNode([]) })],
-            name: 'programA',
-            pdas: [pdaNode({ name: 'pdaA', seeds: [] })],
-            publicKey: '1111',
-        }),
-        [
-            programNode({
-                accounts: [accountNode({ name: 'accountB' })],
-                definedTypes: [definedTypeNode({ name: 'typeB', type: structTypeNode([]) })],
-                name: 'programB',
-                pdas: [pdaNode({ name: 'pdaB', seeds: [] })],
-                publicKey: '2222',
+test('it records program nodes', () => {
+    // Given the following root node containing multiple program nodes.
+    const node = rootNode(programNode({ name: 'programA', publicKey: '1111' }), [
+        programNode({ name: 'programB', publicKey: '2222' }),
+    ]);
+
+    // And a recordLinkablesVisitor extending any visitor.
+    const linkables = new LinkableDictionary();
+    const visitor = recordLinkablesVisitor(voidVisitor(), linkables);
+
+    // When we visit the tree.
+    visit(node, visitor);
+
+    // Then we expect program nodes to be recorded and retrievable.
+    expect(linkables.get(programLinkNode('programA'))).toEqual(node.program);
+    expect(linkables.get(programLinkNode('programB'))).toEqual(node.additionalPrograms[0]);
+});
+
+test('it records account nodes', () => {
+    // Given the following program node containing multiple accounts nodes.
+    const node = programNode({
+        accounts: [accountNode({ name: 'accountA' }), accountNode({ name: 'accountB' })],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // And a recordLinkablesVisitor extending any visitor.
+    const linkables = new LinkableDictionary();
+    const visitor = recordLinkablesVisitor(voidVisitor(), linkables);
+
+    // When we visit the tree.
+    visit(node, visitor);
+
+    // Then we expect account nodes to be recorded and retrievable.
+    expect(linkables.get(accountLinkNode('accountA', 'myProgram'))).toEqual(node.accounts[0]);
+    expect(linkables.get(accountLinkNode('accountB', 'myProgram'))).toEqual(node.accounts[1]);
+});
+
+test('it records defined type nodes', () => {
+    // Given the following program node containing multiple defined type nodes.
+    const node = programNode({
+        definedTypes: [
+            definedTypeNode({ name: 'typeA', type: numberTypeNode('u32') }),
+            definedTypeNode({ name: 'typeB', type: numberTypeNode('u32') }),
+        ],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // And a recordLinkablesVisitor extending any visitor.
+    const linkables = new LinkableDictionary();
+    const visitor = recordLinkablesVisitor(voidVisitor(), linkables);
+
+    // When we visit the tree.
+    visit(node, visitor);
+
+    // Then we expect defined type nodes to be recorded and retrievable.
+    expect(linkables.get(definedTypeLinkNode('typeA', 'myProgram'))).toEqual(node.definedTypes[0]);
+    expect(linkables.get(definedTypeLinkNode('typeB', 'myProgram'))).toEqual(node.definedTypes[1]);
+});
+
+test('it records pda nodes', () => {
+    // Given the following program node containing multiple pda nodes.
+    const node = programNode({
+        name: 'myProgram',
+        pdas: [pdaNode({ name: 'pdaA', seeds: [] }), pdaNode({ name: 'pdaB', seeds: [] })],
+        publicKey: '1111',
+    });
+
+    // And a recordLinkablesVisitor extending any visitor.
+    const linkables = new LinkableDictionary();
+    const visitor = recordLinkablesVisitor(voidVisitor(), linkables);
+
+    // When we visit the tree.
+    visit(node, visitor);
+
+    // Then we expect pda nodes to be recorded and retrievable.
+    expect(linkables.get(pdaLinkNode('pdaA', 'myProgram'))).toEqual(node.pdas[0]);
+    expect(linkables.get(pdaLinkNode('pdaB', 'myProgram'))).toEqual(node.pdas[1]);
+});
+
+test('it records instruction nodes', () => {
+    // Given the following program node containing multiple instruction nodes.
+    const node = programNode({
+        instructions: [instructionNode({ name: 'instructionA' }), instructionNode({ name: 'instructionB' })],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // And a recordLinkablesVisitor extending any visitor.
+    const linkables = new LinkableDictionary();
+    const visitor = recordLinkablesVisitor(voidVisitor(), linkables);
+
+    // When we visit the tree.
+    visit(node, visitor);
+
+    // Then we expect instruction nodes to be recorded and retrievable.
+    expect(linkables.get(instructionLinkNode('instructionA', 'myProgram'))).toEqual(node.instructions[0]);
+    expect(linkables.get(instructionLinkNode('instructionB', 'myProgram'))).toEqual(node.instructions[1]);
+});
+
+test('it records instruction account nodes', () => {
+    // Given the following instruction node containing multiple accounts.
+    const node = programNode({
+        instructions: [
+            instructionNode({
+                accounts: [
+                    instructionAccountNode({ isSigner: true, isWritable: false, name: 'accountA' }),
+                    instructionAccountNode({ isSigner: false, isWritable: true, name: 'accountB' }),
+                ],
+                name: 'myInstruction',
             }),
         ],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // And a recordLinkablesVisitor extending any visitor.
+    const linkables = new LinkableDictionary();
+    const visitor = recordLinkablesVisitor(voidVisitor(), linkables);
+
+    // When we visit the tree.
+    visit(node, visitor);
+
+    // Then we expect instruction account nodes to be recorded and retrievable.
+    const instruction = instructionLinkNode('myInstruction', 'myProgram');
+    expect(linkables.get(instructionAccountLinkNode('accountA', instruction))).toEqual(
+        node.instructions[0].accounts[0],
+    );
+    expect(linkables.get(instructionAccountLinkNode('accountB', instruction))).toEqual(
+        node.instructions[0].accounts[1],
     );
+});
+
+test('it records instruction argument nodes', () => {
+    // Given the following instruction node containing multiple arguments.
+    const node = programNode({
+        instructions: [
+            instructionNode({
+                arguments: [
+                    instructionArgumentNode({ name: 'argumentA', type: numberTypeNode('u32') }),
+                    instructionArgumentNode({ name: 'argumentB', type: numberTypeNode('u32') }),
+                ],
+                name: 'myInstruction',
+            }),
+        ],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
 
     // And a recordLinkablesVisitor extending any visitor.
     const linkables = new LinkableDictionary();
@@ -51,15 +188,14 @@ test('it record all linkable nodes it finds when traversing the tree', () => {
     // When we visit the tree.
     visit(node, visitor);
 
-    // Then we expect all linkable nodes to be recorded.
-    expect(linkables.get(programLinkNode('programA'))).toEqual(node.program);
-    expect(linkables.get(programLinkNode('programB'))).toEqual(node.additionalPrograms[0]);
-    expect(linkables.get(pdaLinkNode('pdaA', 'programA'))).toEqual(node.program.pdas[0]);
-    expect(linkables.get(pdaLinkNode('pdaB', 'programB'))).toEqual(node.additionalPrograms[0].pdas[0]);
-    expect(linkables.get(accountLinkNode('accountA', 'programA'))).toEqual(node.program.accounts[0]);
-    expect(linkables.get(accountLinkNode('accountB', 'programB'))).toEqual(node.additionalPrograms[0].accounts[0]);
-    expect(linkables.get(definedTypeLinkNode('typeA', 'programA'))).toEqual(node.program.definedTypes[0]);
-    expect(linkables.get(definedTypeLinkNode('typeB', 'programB'))).toEqual(node.additionalPrograms[0].definedTypes[0]);
+    // Then we expect instruction argument nodes to be recorded and retrievable.
+    const instruction = instructionLinkNode('myInstruction', 'myProgram');
+    expect(linkables.get(instructionArgumentLinkNode('argumentA', instruction))).toEqual(
+        node.instructions[0].arguments[0],
+    );
+    expect(linkables.get(instructionArgumentLinkNode('argumentB', instruction))).toEqual(
+        node.instructions[0].arguments[1],
+    );
 });
 
 test('it records all linkable before the first visit of the base visitor', () => {
@@ -120,6 +256,43 @@ test('it keeps track of the current program when extending a visitor', () => {
     expect(dictionary.programB).toBe(programB.accounts[0]);
 });
 
+test('it keeps track of the current instruction when extending a visitor', () => {
+    // Given the following program node containing two instructions each containing an account with the same name.
+    const node = programNode({
+        instructions: [
+            instructionNode({
+                accounts: [instructionAccountNode({ isSigner: true, isWritable: false, name: 'someAccount' })],
+                name: 'instructionA',
+            }),
+            instructionNode({
+                accounts: [instructionAccountNode({ isSigner: true, isWritable: false, name: 'someAccount' })],
+                name: 'instructionB',
+            }),
+        ],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // And a recordLinkablesVisitor extending a base visitor that checks
+    // the result of getting the linkable node with the same name for each instruction.
+    const linkables = new LinkableDictionary();
+    const dictionary: Record<string, InstructionAccountNode> = {};
+    const baseVisitor = interceptVisitor(voidVisitor(), (node, next) => {
+        if (isNode(node, 'instructionNode')) {
+            dictionary[node.name] = linkables.getOrThrow(instructionAccountLinkNode('someAccount'));
+        }
+        next(node);
+    });
+    const visitor = recordLinkablesVisitor(baseVisitor, linkables);
+
+    // When we visit the tree.
+    visit(node, visitor);
+
+    // Then we expect each instruction to have its own account.
+    expect(dictionary.instructionA).toBe(node.instructions[0].accounts[0]);
+    expect(dictionary.instructionB).toBe(node.instructions[1].accounts[0]);
+});
+
 test('it does not record linkable types that are not under a program node', () => {
     // Given the following account node that is not under a program node.
     const node = accountNode({ name: 'someAccount' });
@@ -134,3 +307,30 @@ test('it does not record linkable types that are not under a program node', () =
     // Then we expect the account node to not be recorded.
     expect(linkables.has(accountLinkNode('someAccount'))).toBe(false);
 });
+
+test('it can throw an exception when trying to retrieve a missing linked node', () => {
+    // Given the following program node with one account.
+    const node = programNode({
+        accounts: [accountNode({ name: 'myAccount' })],
+        name: 'myProgram',
+        publicKey: '1111',
+    });
+
+    // And a recorded LinkableDictionary.
+    const linkables = new LinkableDictionary();
+    const visitor = recordLinkablesVisitor(voidVisitor(), linkables);
+    visit(node, visitor);
+
+    // When we try to retrieve a missing account node.
+    const getMissingAccount = () => linkables.getOrThrow(accountLinkNode('missingAccount', 'myProgram'));
+
+    // Then we expect an exception to be thrown.
+    expect(getMissingAccount).toThrow(
+        new KinobiError(KINOBI_ERROR__LINKED_NODE_NOT_FOUND, {
+            kind: 'accountLinkNode',
+            linkNode: accountLinkNode('missingAccount', 'myProgram'),
+            name: 'missingAccount',
+            stack: [],
+        }),
+    );
+});