diff --git a/.changeset/hungry-hairs-shave.md b/.changeset/hungry-hairs-shave.md new file mode 100644 index 00000000..66341551 --- /dev/null +++ b/.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` diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index e4a09c77..33364252 100644 --- a/packages/errors/src/context.ts +++ b/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; diff --git a/packages/node-types/src/linkNodes/InstructionAccountLinkNode.ts b/packages/node-types/src/linkNodes/InstructionAccountLinkNode.ts new file mode 100644 index 00000000..07f193e4 --- /dev/null +++ b/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; +} diff --git a/packages/node-types/src/linkNodes/InstructionArgumentLinkNode.ts b/packages/node-types/src/linkNodes/InstructionArgumentLinkNode.ts new file mode 100644 index 00000000..9e5e47dd --- /dev/null +++ b/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; +} diff --git a/packages/node-types/src/linkNodes/InstructionLinkNode.ts b/packages/node-types/src/linkNodes/InstructionLinkNode.ts new file mode 100644 index 00000000..47621ee9 --- /dev/null +++ b/packages/node-types/src/linkNodes/InstructionLinkNode.ts @@ -0,0 +1,12 @@ +import type { CamelCaseString } from '../shared'; +import type { ProgramLinkNode } from './ProgramLinkNode'; + +export interface InstructionLinkNode { + readonly kind: 'instructionLinkNode'; + + // Children. + readonly program?: TProgram; + + // Data. + readonly name: CamelCaseString; +} diff --git a/packages/node-types/src/linkNodes/LinkNode.ts b/packages/node-types/src/linkNodes/LinkNode.ts index a49c034f..77cbe8b2 100644 --- a/packages/node-types/src/linkNodes/LinkNode.ts +++ b/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; diff --git a/packages/node-types/src/linkNodes/index.ts b/packages/node-types/src/linkNodes/index.ts index 30aff187..c056ec68 100644 --- a/packages/node-types/src/linkNodes/index.ts +++ b/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'; diff --git a/packages/nodes/README.md b/packages/nodes/README.md index b6164b04..0032ee96 100644 --- a/packages/nodes/README.md +++ b/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) diff --git a/packages/nodes/docs/linkNodes/AccountLinkNode.md b/packages/nodes/docs/linkNodes/AccountLinkNode.md index 74ee0f3b..99d5dbc3 100644 --- a/packages/nodes/docs/linkNodes/AccountLinkNode.md +++ b/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'); diff --git a/packages/nodes/docs/linkNodes/InstructionAccountLinkNode.md b/packages/nodes/docs/linkNodes/InstructionAccountLinkNode.md new file mode 100644 index 00000000..0efc6a72 --- /dev/null +++ b/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'), +); +``` diff --git a/packages/nodes/docs/linkNodes/InstructionArgumentLinkNode.md b/packages/nodes/docs/linkNodes/InstructionArgumentLinkNode.md new file mode 100644 index 00000000..a5f10af6 --- /dev/null +++ b/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'), +); +``` diff --git a/packages/nodes/docs/linkNodes/InstructionLinkNode.md b/packages/nodes/docs/linkNodes/InstructionLinkNode.md new file mode 100644 index 00000000..01e4e232 --- /dev/null +++ b/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'); +``` diff --git a/packages/nodes/docs/linkNodes/README.md b/packages/nodes/docs/linkNodes/README.md index f337b533..f7a10701 100644 --- a/packages/nodes/docs/linkNodes/README.md +++ b/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) diff --git a/packages/nodes/src/linkNodes/InstructionAccountLinkNode.ts b/packages/nodes/src/linkNodes/InstructionAccountLinkNode.ts new file mode 100644 index 00000000..6cfd26d0 --- /dev/null +++ b/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), + }); +} diff --git a/packages/nodes/src/linkNodes/InstructionArgumentLinkNode.ts b/packages/nodes/src/linkNodes/InstructionArgumentLinkNode.ts new file mode 100644 index 00000000..0a6ac88b --- /dev/null +++ b/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), + }); +} diff --git a/packages/nodes/src/linkNodes/InstructionLinkNode.ts b/packages/nodes/src/linkNodes/InstructionLinkNode.ts new file mode 100644 index 00000000..65921e1f --- /dev/null +++ b/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), + }); +} diff --git a/packages/nodes/src/linkNodes/LinkNode.ts b/packages/nodes/src/linkNodes/LinkNode.ts index 80519ad3..f16621f3 100644 --- a/packages/nodes/src/linkNodes/LinkNode.ts +++ b/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. diff --git a/packages/nodes/src/linkNodes/index.ts b/packages/nodes/src/linkNodes/index.ts index 30aff187..c056ec68 100644 --- a/packages/nodes/src/linkNodes/index.ts +++ b/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'; diff --git a/packages/nodes/test/linkNodes/InstructionAccountLinkNode.test.ts b/packages/nodes/test/linkNodes/InstructionAccountLinkNode.test.ts new file mode 100644 index 00000000..98dcd882 --- /dev/null +++ b/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); +}); diff --git a/packages/nodes/test/linkNodes/InstructionArgumentLinkNode.test.ts b/packages/nodes/test/linkNodes/InstructionArgumentLinkNode.test.ts new file mode 100644 index 00000000..09683ae4 --- /dev/null +++ b/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); +}); diff --git a/packages/nodes/test/linkNodes/InstructionLinkNode.test.ts b/packages/nodes/test/linkNodes/InstructionLinkNode.test.ts new file mode 100644 index 00000000..a1effac6 --- /dev/null +++ b/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); +}); diff --git a/packages/renderers-js-umi/README.md b/packages/renderers-js-umi/README.md index c8bdbf94..0335c481 100644 --- a/packages/renderers-js-umi/README.md +++ b/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>` | `{}` | 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` | `{}` | 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>` | `{}` | 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` | `{}` | 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. | diff --git a/packages/renderers-js-umi/src/getRenderMapVisitor.ts b/packages/renderers-js-umi/src/getRenderMapVisitor.ts index 608bba3b..38366025 100644 --- a/packages/renderers-js-umi/src/getRenderMapVisitor.ts +++ b/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', }; diff --git a/packages/renderers-js-umi/src/utils/linkOverrides.ts b/packages/renderers-js-umi/src/utils/linkOverrides.ts index 5ac790d7..9d4ec9bc 100644 --- a/packages/renderers-js-umi/src/utils/linkOverrides.ts +++ b/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; definedTypes?: Record; + instructions?: Record; pdas?: Record; programs?: Record; resolvers?: Record; }; -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, }); diff --git a/packages/renderers-js/README.md b/packages/renderers-js/README.md index 277fc231..39cb49c8 100644 --- a/packages/renderers-js/README.md +++ b/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>` | `{}` | 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` | `{}` | 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` | `{}` | 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>` | `{}` | 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` | `{}` | 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` | `{}` | 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`. | diff --git a/packages/renderers-js/src/utils/linkOverrides.ts b/packages/renderers-js/src/utils/linkOverrides.ts index 6ddcd3f2..c6e71f36 100644 --- a/packages/renderers-js/src/utils/linkOverrides.ts +++ b/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; definedTypes?: Record; + instructions?: Record; pdas?: Record; programs?: Record; resolvers?: Record; }; -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, }); diff --git a/packages/renderers-rust/README.md b/packages/renderers-rust/README.md index fb765d81..64f8072f 100644 --- a/packages/renderers-rust/README.md +++ b/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>` | `{}` | 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` | `{}` | 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>` | `{}` | 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` | `{}` | 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. | diff --git a/packages/renderers-rust/src/ImportMap.ts b/packages/renderers-rust/src/ImportMap.ts index 9ca7e15f..4600e0df 100644 --- a/packages/renderers-rust/src/ImportMap.ts +++ b/packages/renderers-rust/src/ImportMap.ts @@ -4,6 +4,7 @@ const DEFAULT_MODULE_MAP: Record = { 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', diff --git a/packages/renderers-rust/src/utils/linkOverrides.ts b/packages/renderers-rust/src/utils/linkOverrides.ts index 91fc586f..577ca674 100644 --- a/packages/renderers-rust/src/utils/linkOverrides.ts +++ b/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; definedTypes?: Record; + instructions?: Record; pdas?: Record; programs?: Record; resolvers?: Record; }; -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, }); diff --git a/packages/visitors-core/README.md b/packages/visitors-core/README.md index 43a9d691..0db001e3 100644 --- a/packages/visitors-core/README.md +++ b/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. diff --git a/packages/visitors-core/src/LinkableDictionary.ts b/packages/visitors-core/src/LinkableDictionary.ts index d4bc46fc..7c22f455 100644 --- a/packages/visitors-core/src/LinkableDictionary.ts +++ b/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; definedTypes: Map; + instructions: Map; pdas: Map; 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; + arguments: Map; + instruction: InstructionNode; +}; export class LinkableDictionary { readonly programs: Map = 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; + } } diff --git a/packages/visitors-core/src/NodeStack.ts b/packages/visitors-core/src/NodeStack.ts index 85278e32..31c56f06 100644 --- a/packages/visitors-core/src/NodeStack.ts +++ b/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]; } diff --git a/packages/visitors-core/src/getDebugStringVisitor.ts b/packages/visitors-core/src/getDebugStringVisitor.ts index f693f4ce..521cec3b 100644 --- a/packages/visitors-core/src/getDebugStringVisitor.ts +++ b/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': diff --git a/packages/visitors-core/src/identityVisitor.ts b/packages/visitors-core/src/identityVisitor.ts index 63b6a346..333769f5 100644 --- a/packages/visitors-core/src/identityVisitor.ts +++ b/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( }; } + 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; } diff --git a/packages/visitors-core/src/mergeVisitor.ts b/packages/visitors-core/src/mergeVisitor.ts index d64724d7..a30ad4ee 100644 --- a/packages/visitors-core/src/mergeVisitor.ts +++ b/packages/visitors-core/src/mergeVisitor.ts @@ -350,5 +350,41 @@ export function mergeVisitor( }; } + 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; } diff --git a/packages/visitors-core/test/nodes/linkNodes/AccountLinkNode.test.ts b/packages/visitors-core/test/nodes/linkNodes/AccountLinkNode.test.ts index 74b07efe..90e4cc1a 100644 --- a/packages/visitors-core/test/nodes/linkNodes/AccountLinkNode.test.ts +++ b/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]`, + ); }); diff --git a/packages/visitors-core/test/nodes/linkNodes/DefinedTypeLinkNode.test.ts b/packages/visitors-core/test/nodes/linkNodes/DefinedTypeLinkNode.test.ts index 15289fea..4bd3dcde 100644 --- a/packages/visitors-core/test/nodes/linkNodes/DefinedTypeLinkNode.test.ts +++ b/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]`, + ); }); diff --git a/packages/visitors-core/test/nodes/linkNodes/InstructionAccountLinkNode.test.ts b/packages/visitors-core/test/nodes/linkNodes/InstructionAccountLinkNode.test.ts new file mode 100644 index 00000000..a15034ff --- /dev/null +++ b/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]`, + ); +}); diff --git a/packages/visitors-core/test/nodes/linkNodes/InstructionArgumentLinkNode.test.ts b/packages/visitors-core/test/nodes/linkNodes/InstructionArgumentLinkNode.test.ts new file mode 100644 index 00000000..8c7b1387 --- /dev/null +++ b/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]`, + ); +}); diff --git a/packages/visitors-core/test/nodes/linkNodes/InstructionLinkNode.test.ts b/packages/visitors-core/test/nodes/linkNodes/InstructionLinkNode.test.ts new file mode 100644 index 00000000..5f1daeb5 --- /dev/null +++ b/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]`, + ); +}); diff --git a/packages/visitors-core/test/nodes/linkNodes/PdaLinkNode.test.ts b/packages/visitors-core/test/nodes/linkNodes/PdaLinkNode.test.ts index a515415a..ffc3237a 100644 --- a/packages/visitors-core/test/nodes/linkNodes/PdaLinkNode.test.ts +++ b/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]`, + ); }); diff --git a/packages/visitors-core/test/recordLinkablesVisitor.test.ts b/packages/visitors-core/test/recordLinkablesVisitor.test.ts index 60a1494e..c4d9ec7e 100644 --- a/packages/visitors-core/test/recordLinkablesVisitor.test.ts +++ b/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 = {}; + 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: [], + }), + ); +});