Skip to content
Merged
7 changes: 7 additions & 0 deletions .changeset/wet-meals-work.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
'@codama/visitors-core': minor
'@codama/node-types': minor
'@codama/nodes': minor
---

Add a new InstructionStatusNode to instructions
17 changes: 8 additions & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,15 +114,14 @@ Feel free to PR your own visitor here for others to discover. Note that they are

### Generates program clients

| Visitor | Description | Maintainer |
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------- |
| `@codama/renderers-js` ([docs](https://github.com/codama-idl/renderers-js)) | Generates a JavaScript client compatible with [Solana Kit](https://www.solanakit.com/). | [Anza](https://www.anza.xyz/) |
| `@codama/renderers-js-umi` ([docs](https://github.com/codama-idl/renderers-js-umi)) | Generates a JavaScript client compatible with [the Umi framework](https://developers.metaplex.com/umi). | [Metaplex](https://www.metaplex.com/) |
| `@codama/renderers-rust` ([docs](https://github.com/codama-idl/renderers-rust)) | Generates a Rust client compatible with [the Solana SDK](https://github.com/anza-xyz/solana-sdk). | [Anza](https://www.anza.xyz/) |
| `@codama/renderers-vixen-parser` ([docs](https://github.com/codama-idl/renderers-vixen-parser)) | Generates [Yellowstone](https://github.com/rpcpool/yellowstone-grpc) account and instruction parsers. | [Triton One](https://triton.one/) |
| `@limechain/codama-dart` ([docs](https://github.com/limechain/codama-dart))| Generates a Dart client. | [LimeChain](https://github.com/limechain/)|
| `codama-py` ([docs](https://github.com/Solana-ZH/codama-py)) | Generates a Python client. | [Solar](https://github.com/Solana-ZH) |

| Visitor | Description | Maintainer |
| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | ------------------------------------------ |
| `@codama/renderers-js` ([docs](https://github.com/codama-idl/renderers-js)) | Generates a JavaScript client compatible with [Solana Kit](https://www.solanakit.com/). | [Anza](https://www.anza.xyz/) |
| `@codama/renderers-js-umi` ([docs](https://github.com/codama-idl/renderers-js-umi)) | Generates a JavaScript client compatible with [the Umi framework](https://developers.metaplex.com/umi). | [Metaplex](https://www.metaplex.com/) |
| `@codama/renderers-rust` ([docs](https://github.com/codama-idl/renderers-rust)) | Generates a Rust client compatible with [the Solana SDK](https://github.com/anza-xyz/solana-sdk). | [Anza](https://www.anza.xyz/) |
| `@codama/renderers-vixen-parser` ([docs](https://github.com/codama-idl/renderers-vixen-parser)) | Generates [Yellowstone](https://github.com/rpcpool/yellowstone-grpc) account and instruction parsers. | [Triton One](https://triton.one/) |
| `@limechain/codama-dart` ([docs](https://github.com/limechain/codama-dart)) | Generates a Dart client. | [LimeChain](https://github.com/limechain/) |
| `codama-py` ([docs](https://github.com/Solana-ZH/codama-py)) | Generates a Python client. | [Solar](https://github.com/Solana-ZH) |

### Provides utility

Expand Down
2 changes: 2 additions & 0 deletions packages/node-types/src/InstructionNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type { InstructionAccountNode } from './InstructionAccountNode';
import type { InstructionArgumentNode } from './InstructionArgumentNode';
import type { InstructionByteDeltaNode } from './InstructionByteDeltaNode';
import type { InstructionRemainingAccountsNode } from './InstructionRemainingAccountsNode';
import type { InstructionStatusNode } from './InstructionStatusNode';
import type { CamelCaseString, Docs } from './shared';

type SubInstructionNode = InstructionNode;
Expand Down Expand Up @@ -34,5 +35,6 @@ export interface InstructionNode<
readonly remainingAccounts?: TRemainingAccounts;
readonly byteDeltas?: TByteDeltas;
readonly discriminators?: TDiscriminators;
readonly status?: InstructionStatusNode;
readonly subInstructions?: TSubInstructions;
}
9 changes: 9 additions & 0 deletions packages/node-types/src/InstructionStatusNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import type { InstructionLifecycle } from './shared';

export interface InstructionStatusNode {
readonly kind: 'instructionStatusNode';

// Data.
readonly lifecycle: InstructionLifecycle;
readonly message?: string;
}
2 changes: 2 additions & 0 deletions packages/node-types/src/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import type { InstructionArgumentNode } from './InstructionArgumentNode';
import type { InstructionByteDeltaNode } from './InstructionByteDeltaNode';
import type { InstructionNode } from './InstructionNode';
import type { InstructionRemainingAccountsNode } from './InstructionRemainingAccountsNode';
import type { InstructionStatusNode } from './InstructionStatusNode';
import type { RegisteredLinkNode } from './linkNodes/LinkNode';
import type { PdaNode } from './PdaNode';
import type { RegisteredPdaSeedNode } from './pdaSeedNodes/PdaSeedNode';
Expand All @@ -28,6 +29,7 @@ export type Node =
| InstructionByteDeltaNode
| InstructionNode
| InstructionRemainingAccountsNode
| InstructionStatusNode
| PdaNode
| ProgramNode
| RegisteredContextualValueNode
Expand Down
1 change: 1 addition & 0 deletions packages/node-types/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './InstructionArgumentNode';
export * from './InstructionByteDeltaNode';
export * from './InstructionNode';
export * from './InstructionRemainingAccountsNode';
export * from './InstructionStatusNode';
export * from './Node';
export * from './PdaNode';
export * from './ProgramNode';
Expand Down
1 change: 1 addition & 0 deletions packages/node-types/src/shared/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export * from './bytesEncoding';
export * from './docs';
export * from './instructionLifecycle';
export * from './stringCases';
export * from './version';
1 change: 1 addition & 0 deletions packages/node-types/src/shared/instructionLifecycle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export type InstructionLifecycle = 'archived' | 'deprecated' | 'draft' | 'live';
45 changes: 44 additions & 1 deletion packages/nodes/docs/InstructionNode.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ This node represents an instruction in a program.
| `remainingAccounts` | [`InstructionRemainingAccountsNode`](./InstructionRemainingAccountsNode.md)[] | (Optional) The list of dynamic remaining accounts requirements for the instruction. For instance, an instruction may have a variable number of signers at the end of the accounts list. |
| `byteDeltas` | [`InstructionByteDeltaNode`](./InstructionByteDeltaNode.md)[] | (Optional) The list of byte variations that the instruction causes. They should all be added together unless the `subtract` attribute is used. |
| `discriminators` | [`DiscriminatorNode`](./DiscriminatorNode.md)[] | (Optional) The nodes that distinguish this instruction from others in the program. If multiple discriminators are provided, they are combined using a logical AND operation. |
| `status` | [`InstructionStatusNode`](./InstructionStatusNode.md) | (Optional) The status of the instruction and an optional message about that status. |
| `subInstructions` | [`InstructionNode`](./InstructionNode.md)[] | (Optional) A list of nested instructions should this instruction be split into multiple sub-instructions to define distinct scenarios. |

## Functions
Expand Down Expand Up @@ -121,7 +122,7 @@ instructionNode({
});
```

### An instruction with nested versionned instructions
### An instruction with nested versioned instructions

```ts
instructionNode({
Expand Down Expand Up @@ -167,3 +168,45 @@ instructionNode({
],
});
```

### A deprecated instruction

```ts
instructionNode({
name: 'oldIncrement',
status: instructionStatusNode(
'deprecated',
'Use the `increment` instruction instead. This will be removed in v3.0.0.',
),
accounts: [instructionAccountNode({ name: 'counter', isWritable: true, isSigner: false })],
arguments: [instructionArgumentNode({ name: 'amount', type: numberTypeNode('u8') })],
});
```

### An archived instruction

```ts
instructionNode({
name: 'legacyTransfer',
status: instructionStatusNode(
'archived',
'This instruction was removed in v2.0.0. It is kept here for historical parsing.',
),
accounts: [
instructionAccountNode({ name: 'source', isWritable: true, isSigner: true }),
instructionAccountNode({ name: 'destination', isWritable: true, isSigner: false }),
],
arguments: [instructionArgumentNode({ name: 'amount', type: numberTypeNode('u64') })],
});
```

### A draft instruction

```ts
instructionNode({
name: 'experimentalFeature',
status: instructionStatusNode('draft', 'This instruction is under development and may change.'),
accounts: [instructionAccountNode({ name: 'config', isWritable: true, isSigner: true })],
arguments: [],
});
```
81 changes: 81 additions & 0 deletions packages/nodes/docs/InstructionStatusNode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# `InstructionStatusNode`

This node represents the status of an instruction along with an optional message.

## Attributes

### Data

| Attribute | Type | Description |
| ----------- | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `kind` | `"instructionStatusNode"` | The node discriminator. |
| `lifecycle` | `"live"` \| `"deprecated"` \| `"archived"` \| `"draft"` | The lifecycle status of the instruction. `"live"` means accessible (the default), `"deprecated"` means about to be archived, `"archived"` means no longer accessible but kept for historical parsing, `"draft"` means not fully implemented yet. |
| `message` | `string` | (Optional) Additional information about the current status for program consumers. |

## Functions

### `instructionStatusNode(lifecycle, message?)`

Helper function that creates an `InstructionStatusNode` object.

```ts
const statusNode = instructionStatusNode('deprecated', 'Use the newInstruction instead');
```

## Examples

### A live instruction (no status needed)

For live instructions, you typically don't need to set a status at all:

```ts
instructionNode({
name: 'transfer',
accounts: [...],
arguments: [...],
});
```

### A deprecated instruction

```ts
instructionNode({
name: 'oldTransfer',
status: instructionStatusNode('deprecated', 'Use the `transfer` instruction instead. This will be removed in v3.0.0.'),
accounts: [...],
arguments: [...],
});
```

### An archived instruction

```ts
instructionNode({
name: 'legacyTransfer',
status: instructionStatusNode('archived', 'This instruction was removed in v2.0.0. It is kept here for historical parsing.'),
accounts: [...],
arguments: [...],
});
```

### A draft instruction

```ts
instructionNode({
name: 'experimentalFeature',
status: instructionStatusNode('draft', 'This instruction is under development and may change.'),
accounts: [...],
arguments: [...],
});
```

### Status without a message

```ts
instructionNode({
name: 'someInstruction',
status: instructionStatusNode('deprecated'),
accounts: [...],
arguments: [...],
});
```
1 change: 1 addition & 0 deletions packages/nodes/src/InstructionNode.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ export function instructionNode<
name: camelCase(input.name),
docs: parseDocs(input.docs),
optionalAccountStrategy: parseOptionalAccountStrategy(input.optionalAccountStrategy),
...(input.status !== undefined && { status: input.status }),

// Children.
accounts: (input.accounts ?? []) as TAccounts,
Expand Down
11 changes: 11 additions & 0 deletions packages/nodes/src/InstructionStatusNode.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import type { InstructionLifecycle, InstructionStatusNode } from '@codama/node-types';

export function instructionStatusNode(lifecycle: InstructionLifecycle, message?: string): InstructionStatusNode {
return Object.freeze({
kind: 'instructionStatusNode',

// Data.
lifecycle,
...(message !== undefined && { message }),
});
}
1 change: 1 addition & 0 deletions packages/nodes/src/Node.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export const REGISTERED_NODE_KINDS = [
'instructionByteDeltaNode' as const,
'instructionNode' as const,
'instructionRemainingAccountsNode' as const,
'instructionStatusNode' as const,
'errorNode' as const,
'definedTypeNode' as const,
];
Expand Down
1 change: 1 addition & 0 deletions packages/nodes/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export * from './InstructionArgumentNode';
export * from './InstructionByteDeltaNode';
export * from './InstructionNode';
export * from './InstructionRemainingAccountsNode';
export * from './InstructionStatusNode';
export * from './Node';
export * from './PdaNode';
export * from './ProgramNode';
Expand Down
54 changes: 53 additions & 1 deletion packages/nodes/test/InstructionNode.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { expect, test } from 'vitest';

import { instructionNode } from '../src';
import { instructionNode, instructionStatusNode } from '../src';

test('it returns the right node kind', () => {
const node = instructionNode({ name: 'foo' });
Expand All @@ -11,3 +11,55 @@ test('it returns a frozen object', () => {
const node = instructionNode({ name: 'foo' });
expect(Object.isFrozen(node)).toBe(true);
});

test('it defaults to no status', () => {
const node = instructionNode({ name: 'foo' });
expect(node.status).toBeUndefined();
});

test('it can have a live status', () => {
const statusMode = instructionStatusNode('live');
const node = instructionNode({ name: 'foo', status: statusMode });
expect(node.status).toBe(statusMode);
expect(node.status?.lifecycle).toBe('live');
});

test('it can have a deprecated status with message', () => {
const statusMode = instructionStatusNode('deprecated', 'Use the newFoo instruction instead.');
const node = instructionNode({ name: 'foo', status: statusMode });
expect(node.status).toBe(statusMode);
expect(node.status?.lifecycle).toBe('deprecated');
expect(node.status?.message).toBe('Use the newFoo instruction instead.');
});

test('it can have an archived status with message', () => {
const statusMode = instructionStatusNode('archived', 'This instruction was removed in v2.0.0.');
const node = instructionNode({ name: 'foo', status: statusMode });
expect(node.status).toBe(statusMode);
expect(node.status?.lifecycle).toBe('archived');
expect(node.status?.message).toBe('This instruction was removed in v2.0.0.');
});

test('it can have a draft status with message', () => {
const statusMode = instructionStatusNode('draft', 'This instruction is under development.');
const node = instructionNode({ name: 'foo', status: statusMode });
expect(node.status).toBe(statusMode);
expect(node.status?.lifecycle).toBe('draft');
expect(node.status?.message).toBe('This instruction is under development.');
});

test('it can have a status without a message', () => {
const statusMode = instructionStatusNode('deprecated');
const node = instructionNode({ name: 'foo', status: statusMode });
expect(node.status).toBe(statusMode);
expect(node.status?.lifecycle).toBe('deprecated');
expect(node.status?.message).toBeUndefined();
});

test('it can have an empty message', () => {
const statusMode = instructionStatusNode('deprecated', '');
const node = instructionNode({ name: 'foo', status: statusMode });
expect(node.status).toBe(statusMode);
expect(node.status?.lifecycle).toBe('deprecated');
expect(node.status?.message).toBe('');
});
25 changes: 25 additions & 0 deletions packages/nodes/test/InstructionStatusNode.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { expect, test } from 'vitest';

import { instructionStatusNode } from '../src';

test('it returns the right node kind', () => {
const node = instructionStatusNode('live');
expect(node.kind).toBe('instructionStatusNode');
});

test('it returns a frozen object', () => {
const node = instructionStatusNode('live');
expect(Object.isFrozen(node)).toBe(true);
});

test('it can have a status with message', () => {
const node = instructionStatusNode('deprecated', 'Use newInstruction');
expect(node.lifecycle).toBe('deprecated');
expect(node.message).toBe('Use newInstruction');
});

test('it can have a status without message', () => {
const node = instructionStatusNode('archived');
expect(node.lifecycle).toBe('archived');
expect(node.message).toBeUndefined();
});
2 changes: 2 additions & 0 deletions packages/visitors-core/src/getDebugStringVisitor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ function getNodeDetails(node: Node): string[] {
];
case 'instructionByteDeltaNode':
return [...(node.subtract ? ['subtract'] : []), ...(node.withHeader ? ['withHeader'] : [])];
case 'instructionStatusNode':
return [node.lifecycle, ...(node.message ? [node.message] : [])];
case 'errorNode':
return [node.code.toString(), node.name];
case 'accountLinkNode':
Expand Down
Loading