diff --git a/.changeset/wicked-kangaroos-agree.md b/.changeset/wicked-kangaroos-agree.md new file mode 100644 index 000000000..cac765eb5 --- /dev/null +++ b/.changeset/wicked-kangaroos-agree.md @@ -0,0 +1,9 @@ +--- +"@codama/errors": patch +"@codama/node-types": minor +"@codama/nodes-from-anchor": patch +"@codama/nodes": minor +"@codama/visitors-core": patch +--- + +Add new `EventNode` to `ProgramNode` and update the Anchor adapter accordingly. diff --git a/README.md b/README.md index 78b532346..1c30eafbe 100644 --- a/README.md +++ b/README.md @@ -114,15 +114,15 @@ Feel free to PR your own visitor here for others to discover. Note that they are ### Generates program clients -| Visitor | Description | Maintainer | -| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | -------------------------------------------- | -| `@codama/renderers-go` ([docs](https://github.com/codama-idl/renderers-go)) | Generates a Go client compatible with [the Solana Go SDK](https://github.com/gagliardetto/solana-go). | [Sonic](hhttps://github.com/sonicfromnewyoke)| -| `@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-go` ([docs](https://github.com/codama-idl/renderers-go)) | Generates a Go client compatible with [the Solana Go SDK](https://github.com/gagliardetto/solana-go). | [Sonic](hhttps://github.com/sonicfromnewyoke) | +| `@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 diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 4ed2f20d5..d26acdab7 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -62,6 +62,7 @@ export const CODAMA_ERROR__ANCHOR__TYPE_PATH_MISSING = 2100003; export const CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED = 2100004; export const CODAMA_ERROR__ANCHOR__PROGRAM_ID_KIND_UNIMPLEMENTED = 2100005; export const CODAMA_ERROR__ANCHOR__GENERIC_TYPE_MISSING = 2100006; +export const CODAMA_ERROR__ANCHOR__EVENT_TYPE_MISSING = 2100007; // Renderers-related errors. // Reserve error codes in the range [2800000-2800999]. @@ -86,6 +87,7 @@ export const CODAMA_ERROR__RENDERERS__MISSING_DEPENDENCY_VERSIONS = 2800001; export type CodamaErrorCode = | typeof CODAMA_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING | typeof CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING + | typeof CODAMA_ERROR__ANCHOR__EVENT_TYPE_MISSING | typeof CODAMA_ERROR__ANCHOR__GENERIC_TYPE_MISSING | typeof CODAMA_ERROR__ANCHOR__PROGRAM_ID_KIND_UNIMPLEMENTED | typeof CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index 5f040ba97..255335f90 100644 --- a/packages/errors/src/context.ts +++ b/packages/errors/src/context.ts @@ -22,6 +22,7 @@ import { import { CODAMA_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING, CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING, + CODAMA_ERROR__ANCHOR__EVENT_TYPE_MISSING, CODAMA_ERROR__ANCHOR__GENERIC_TYPE_MISSING, CODAMA_ERROR__ANCHOR__PROGRAM_ID_KIND_UNIMPLEMENTED, CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED, @@ -71,6 +72,9 @@ export type CodamaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{ [CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING]: { name: string; }; + [CODAMA_ERROR__ANCHOR__EVENT_TYPE_MISSING]: { + name: string; + }; [CODAMA_ERROR__ANCHOR__GENERIC_TYPE_MISSING]: { name: string; }; diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index accfa4c60..d0eba5940 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -6,6 +6,7 @@ import { CODAMA_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING, CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING, + CODAMA_ERROR__ANCHOR__EVENT_TYPE_MISSING, CODAMA_ERROR__ANCHOR__GENERIC_TYPE_MISSING, CODAMA_ERROR__ANCHOR__PROGRAM_ID_KIND_UNIMPLEMENTED, CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED, @@ -51,6 +52,7 @@ export const CodamaErrorMessages: Readonly<{ }> = { [CODAMA_ERROR__ANCHOR__ACCOUNT_TYPE_MISSING]: 'Account type [$name] is missing from the IDL types.', [CODAMA_ERROR__ANCHOR__ARGUMENT_TYPE_MISSING]: 'Argument name [$name] is missing from the instruction definition.', + [CODAMA_ERROR__ANCHOR__EVENT_TYPE_MISSING]: 'Event type [$name] is missing from the IDL types.', [CODAMA_ERROR__ANCHOR__GENERIC_TYPE_MISSING]: 'Generic type [$name] is missing from the IDL types.', [CODAMA_ERROR__ANCHOR__PROGRAM_ID_KIND_UNIMPLEMENTED]: 'Program ID kind [$kind] is not implemented.', [CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED]: 'Seed kind [$kind] is not implemented.', diff --git a/packages/node-types/src/EventNode.ts b/packages/node-types/src/EventNode.ts new file mode 100644 index 000000000..40794ffc0 --- /dev/null +++ b/packages/node-types/src/EventNode.ts @@ -0,0 +1,18 @@ +import type { DiscriminatorNode } from './discriminatorNodes'; +import type { CamelCaseString, Docs } from './shared'; +import type { TypeNode } from './typeNodes'; + +export interface EventNode< + TData extends TypeNode = TypeNode, + TDiscriminators extends DiscriminatorNode[] | undefined = DiscriminatorNode[] | undefined, +> { + readonly kind: 'eventNode'; + + // Data. + readonly name: CamelCaseString; + readonly docs?: Docs; + + // Children. + readonly data: TData; + readonly discriminators?: TDiscriminators; +} diff --git a/packages/node-types/src/Node.ts b/packages/node-types/src/Node.ts index c082abf8b..341f3b0e8 100644 --- a/packages/node-types/src/Node.ts +++ b/packages/node-types/src/Node.ts @@ -4,6 +4,7 @@ import type { RegisteredCountNode } from './countNodes/CountNode'; import type { DefinedTypeNode } from './DefinedTypeNode'; import type { RegisteredDiscriminatorNode } from './discriminatorNodes/DiscriminatorNode'; import type { ErrorNode } from './ErrorNode'; +import type { EventNode } from './EventNode'; import type { InstructionAccountNode } from './InstructionAccountNode'; import type { InstructionArgumentNode } from './InstructionArgumentNode'; import type { InstructionByteDeltaNode } from './InstructionByteDeltaNode'; @@ -24,6 +25,7 @@ export type Node = | AccountNode | DefinedTypeNode | ErrorNode + | EventNode | InstructionAccountNode | InstructionArgumentNode | InstructionByteDeltaNode diff --git a/packages/node-types/src/ProgramNode.ts b/packages/node-types/src/ProgramNode.ts index 515ebbbb6..815c80a3a 100644 --- a/packages/node-types/src/ProgramNode.ts +++ b/packages/node-types/src/ProgramNode.ts @@ -1,6 +1,7 @@ import type { AccountNode } from './AccountNode'; import type { DefinedTypeNode } from './DefinedTypeNode'; import type { ErrorNode } from './ErrorNode'; +import type { EventNode } from './EventNode'; import type { InstructionNode } from './InstructionNode'; import type { PdaNode } from './PdaNode'; import type { CamelCaseString, Docs, ProgramVersion } from './shared'; @@ -11,6 +12,7 @@ export interface ProgramNode< TInstructions extends InstructionNode[] = InstructionNode[], TDefinedTypes extends DefinedTypeNode[] = DefinedTypeNode[], TErrors extends ErrorNode[] = ErrorNode[], + TEvents extends EventNode[] = EventNode[], > { readonly kind: 'programNode'; @@ -26,5 +28,6 @@ export interface ProgramNode< readonly instructions: TInstructions; readonly definedTypes: TDefinedTypes; readonly pdas: TPdas; + readonly events: TEvents; readonly errors: TErrors; } diff --git a/packages/node-types/src/index.ts b/packages/node-types/src/index.ts index 8b4a8d2d6..cc9776610 100644 --- a/packages/node-types/src/index.ts +++ b/packages/node-types/src/index.ts @@ -1,6 +1,7 @@ export * from './AccountNode'; export * from './DefinedTypeNode'; export * from './ErrorNode'; +export * from './EventNode'; export * from './InstructionAccountNode'; export * from './InstructionArgumentNode'; export * from './InstructionByteDeltaNode'; diff --git a/packages/nodes-from-anchor/src/v01/EventNode.ts b/packages/nodes-from-anchor/src/v01/EventNode.ts new file mode 100644 index 000000000..8c8c1dc50 --- /dev/null +++ b/packages/nodes-from-anchor/src/v01/EventNode.ts @@ -0,0 +1,38 @@ +import { CODAMA_ERROR__ANCHOR__EVENT_TYPE_MISSING, CodamaError } from '@codama/errors'; +import { + bytesTypeNode, + camelCase, + constantDiscriminatorNode, + constantValueNode, + EventNode, + eventNode, + fixedSizeTypeNode, + hiddenPrefixTypeNode, +} from '@codama/nodes'; + +import { getAnchorDiscriminatorV01 } from './../discriminators'; +import type { IdlV01Event, IdlV01TypeDef } from './idl'; +import { typeNodeFromAnchorV01 } from './typeNodes'; +import type { GenericsV01 } from './unwrapGenerics'; + +export function eventNodeFromAnchorV01(idl: IdlV01Event, types: IdlV01TypeDef[], generics: GenericsV01): EventNode { + const name = camelCase(idl.name); + const type = types.find(candidate => candidate.name === idl.name); + + if (!type) { + throw new CodamaError(CODAMA_ERROR__ANCHOR__EVENT_TYPE_MISSING, { name: idl.name }); + } + + const data = typeNodeFromAnchorV01(type.type, generics); + const discriminator = getAnchorDiscriminatorV01(idl.discriminator); + const discriminatorConstant = constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), idl.discriminator.length), + discriminator, + ); + + return eventNode({ + data: hiddenPrefixTypeNode(data, [discriminatorConstant]), + discriminators: [constantDiscriminatorNode(discriminatorConstant)], + name, + }); +} diff --git a/packages/nodes-from-anchor/src/v01/ProgramNode.ts b/packages/nodes-from-anchor/src/v01/ProgramNode.ts index acf6aee68..b393ab206 100644 --- a/packages/nodes-from-anchor/src/v01/ProgramNode.ts +++ b/packages/nodes-from-anchor/src/v01/ProgramNode.ts @@ -3,6 +3,7 @@ import { ProgramNode, programNode, ProgramVersion } from '@codama/nodes'; import { accountNodeFromAnchorV01 } from './AccountNode'; import { definedTypeNodeFromAnchorV01 } from './DefinedTypeNode'; import { errorNodeFromAnchorV01 } from './ErrorNode'; +import { eventNodeFromAnchorV01 } from './EventNode'; import { IdlV01 } from './idl'; import { instructionNodeFromAnchorV01 } from './InstructionNode'; import { extractGenerics } from './unwrapGenerics'; @@ -10,10 +11,14 @@ import { extractGenerics } from './unwrapGenerics'; export function programNodeFromAnchorV01(idl: IdlV01): ProgramNode { const [types, generics] = extractGenerics(idl.types ?? []); const accounts = idl.accounts ?? []; + const events = idl.events ?? []; const instructions = idl.instructions ?? []; const errors = idl.errors ?? []; - const filteredTypes = types.filter(type => !accounts.some(account => account.name === type.name)); + const filteredTypes = types.filter( + type => + !accounts.some(account => account.name === type.name) && !events.some(event => event.name === type.name), + ); const definedTypes = filteredTypes.map(type => definedTypeNodeFromAnchorV01(type, generics)); const accountNodes = accounts.map(account => accountNodeFromAnchorV01(account, types, generics)); @@ -21,6 +26,7 @@ export function programNodeFromAnchorV01(idl: IdlV01): ProgramNode { accounts: accountNodes, definedTypes, errors: errors.map(errorNodeFromAnchorV01), + events: events.map(event => eventNodeFromAnchorV01(event, types, generics)), instructions: instructions.map(instruction => instructionNodeFromAnchorV01(instruction, generics)), name: idl.metadata.name, origin: 'anchor', diff --git a/packages/nodes-from-anchor/src/v01/index.ts b/packages/nodes-from-anchor/src/v01/index.ts index 939271c9d..9ce1db98e 100644 --- a/packages/nodes-from-anchor/src/v01/index.ts +++ b/packages/nodes-from-anchor/src/v01/index.ts @@ -1,5 +1,6 @@ export * from './AccountNode'; export * from './DefinedTypeNode'; +export * from './EventNode'; export * from './ErrorNode'; export * from './idl'; export * from './InstructionAccountNode'; diff --git a/packages/nodes-from-anchor/test/v01/EventNode.test.ts b/packages/nodes-from-anchor/test/v01/EventNode.test.ts new file mode 100644 index 000000000..6c7548142 --- /dev/null +++ b/packages/nodes-from-anchor/test/v01/EventNode.test.ts @@ -0,0 +1,118 @@ +import { CODAMA_ERROR__ANCHOR__EVENT_TYPE_MISSING, CodamaError } from '@codama/errors'; +import { constantDiscriminatorNode, constantValueNode } from '@codama/nodes'; +import { + bytesTypeNode, + eventNode, + fixedSizeTypeNode, + hiddenPrefixTypeNode, + numberTypeNode, + structFieldTypeNode, + structTypeNode, + tupleTypeNode, +} from '@codama/nodes'; +import { expect, test } from 'vitest'; + +import { eventNodeFromAnchorV01, GenericsV01, getAnchorDiscriminatorV01 } from '../../src'; + +const generics = {} as GenericsV01; + +test('it creates event nodes with anchor discriminators', () => { + const node = eventNodeFromAnchorV01( + { + discriminator: [246, 28, 6, 87, 251, 45, 50, 42], + name: 'MyEvent', + }, + [ + { + docs: [], + name: 'MyEvent', + type: { + fields: [ + { + name: 'amount', + type: 'u32', + }, + ], + kind: 'struct', + }, + }, + ], + generics, + ); + + expect(node).toEqual( + eventNode({ + data: hiddenPrefixTypeNode( + structTypeNode([structFieldTypeNode({ name: 'amount', type: numberTypeNode('u32') })]), + [ + constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), 8), + getAnchorDiscriminatorV01([246, 28, 6, 87, 251, 45, 50, 42]), + ), + ], + ), + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), 8), + getAnchorDiscriminatorV01([246, 28, 6, 87, 251, 45, 50, 42]), + ), + ), + ], + name: 'myEvent', + }), + ); +}); + +test('it creates tuple event nodes with anchor discriminators', () => { + const node = eventNodeFromAnchorV01( + { + discriminator: [246, 28, 6, 87, 251, 45, 50, 42], + name: 'TupleEvent', + }, + [ + { + docs: [], + name: 'TupleEvent', + type: { + fields: ['u32', 'u64'], + kind: 'struct', + }, + }, + ], + generics, + ); + + expect(node).toEqual( + eventNode({ + data: hiddenPrefixTypeNode(tupleTypeNode([numberTypeNode('u32'), numberTypeNode('u64')]), [ + constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), 8), + getAnchorDiscriminatorV01([246, 28, 6, 87, 251, 45, 50, 42]), + ), + ]), + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), 8), + getAnchorDiscriminatorV01([246, 28, 6, 87, 251, 45, 50, 42]), + ), + ), + ], + name: 'tupleEvent', + }), + ); +}); + +test('it throws when the backing event type is missing', () => { + expect(() => + eventNodeFromAnchorV01( + { + discriminator: [246, 28, 6, 87, 251, 45, 50, 42], + name: 'MissingEvent', + }, + [], + generics, + ), + ).toThrow(new CodamaError(CODAMA_ERROR__ANCHOR__EVENT_TYPE_MISSING, { name: 'MissingEvent' })); +}); diff --git a/packages/nodes-from-anchor/test/v01/ProgramNode.test.ts b/packages/nodes-from-anchor/test/v01/ProgramNode.test.ts index 97768bda4..2441cedf3 100644 --- a/packages/nodes-from-anchor/test/v01/ProgramNode.test.ts +++ b/packages/nodes-from-anchor/test/v01/ProgramNode.test.ts @@ -4,16 +4,20 @@ import { argumentValueNode, arrayTypeNode, bytesTypeNode, + constantDiscriminatorNode, constantPdaSeedNodeFromBytes, + constantValueNode, definedTypeLinkNode, definedTypeNode, enumEmptyVariantTypeNode, enumTupleVariantTypeNode, enumTypeNode, errorNode, + eventNode, fieldDiscriminatorNode, fixedCountNode, fixedSizeTypeNode, + hiddenPrefixTypeNode, instructionAccountNode, instructionArgumentNode, instructionNode, @@ -37,6 +41,7 @@ test('it creates program nodes', () => { accounts: [{ discriminator: [246, 28, 6, 87, 251, 45, 50, 42], name: 'MyAccount' }], address: '1111', errors: [{ code: 42, msg: 'my error message', name: 'myError' }], + events: [{ discriminator: [1, 2, 3, 4, 5, 6, 7, 8], name: 'MyEvent' }], instructions: [ { accounts: [ @@ -68,7 +73,10 @@ test('it creates program nodes', () => { }, ], metadata: { name: 'my_program', spec: '0.1.0', version: '1.2.3' }, - types: [{ name: 'MyAccount', type: { fields: [{ name: 'delegate', type: 'pubkey' }], kind: 'struct' } }], + types: [ + { name: 'MyAccount', type: { fields: [{ name: 'delegate', type: 'pubkey' }], kind: 'struct' } }, + { name: 'MyEvent', type: { fields: [{ name: 'amount', type: 'u64' }], kind: 'struct' } }, + ], }); expect(node).toEqual( @@ -100,6 +108,33 @@ test('it creates program nodes', () => { name: 'myError', }), ], + events: [ + eventNode({ + data: hiddenPrefixTypeNode( + structTypeNode([ + structFieldTypeNode({ + name: 'amount', + type: numberTypeNode('u64'), + }), + ]), + [ + constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), 8), + getAnchorDiscriminatorV01([1, 2, 3, 4, 5, 6, 7, 8]), + ), + ], + ), + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), 8), + getAnchorDiscriminatorV01([1, 2, 3, 4, 5, 6, 7, 8]), + ), + ), + ], + name: 'myEvent', + }), + ], instructions: [ instructionNode({ accounts: [ diff --git a/packages/nodes/README.md b/packages/nodes/README.md index d7982cf33..1235f251c 100644 --- a/packages/nodes/README.md +++ b/packages/nodes/README.md @@ -31,6 +31,7 @@ Below are all of the available nodes and their documentation. Also note that you - [`AccountNode`](./docs/AccountNode.md) - [`DefinedTypeNode`](./docs/DefinedTypeNode.md) - [`ErrorNode`](./docs/ErrorNode.md) +- [`EventNode`](./docs/EventNode.md) - [`InstructionAccountNode`](./docs/InstructionAccountNode.md) - [`InstructionArgumentNode`](./docs/InstructionArgumentNode.md) - [`InstructionByteDeltaNode`](./docs/InstructionByteDeltaNode.md) diff --git a/packages/nodes/docs/EventNode.md b/packages/nodes/docs/EventNode.md new file mode 100644 index 000000000..cdf15b124 --- /dev/null +++ b/packages/nodes/docs/EventNode.md @@ -0,0 +1,66 @@ +# `EventNode` + +This node represents an event emitted by a program. + +## Attributes + +### Data + +| Attribute | Type | Description | +| --------- | ----------------- | ------------------------------------------------ | +| `kind` | `"eventNode"` | The node discriminator. | +| `name` | `CamelCaseString` | The name of the event. | +| `docs` | `string[]` | Additional Markdown documentation for the event. | + +### Children + +| Attribute | Type | Description | +| ---------------- | ------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `data` | [`TypeNode`](./typeNodes/README.md) | The type node that describes the event payload. | +| `discriminators` | [`DiscriminatorNode`](./discriminatorNodes/README.md)[] | (Optional) The nodes that distinguish this event from others in the program. If multiple discriminators are provided, they are combined using a logical AND operation. | + +## Functions + +### `eventNode(input)` + +Helper function that creates an `EventNode` object from an input object. + +```ts +const node = eventNode({ + name: 'transferEvent', + data: structTypeNode([ + structFieldTypeNode({ name: 'authority', type: publicKeyTypeNode() }), + structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') }), + ]), +}); +``` + +## Examples + +### An event with a struct payload + +```ts +eventNode({ + name: 'transferEvent', + data: structTypeNode([ + structFieldTypeNode({ name: 'authority', type: publicKeyTypeNode() }), + structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') }), + ]), +}); +``` + +### An event with a hidden prefix discriminator + +```ts +eventNode({ + name: 'transferEvent', + data: hiddenPrefixTypeNode(structTypeNode([structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') })]), [ + constantValueNode(fixedSizeTypeNode(bytesTypeNode(), 8), bytesValueNode('base16', '0102030405060708')), + ]), + discriminators: [ + constantDiscriminatorNode( + constantValueNode(fixedSizeTypeNode(bytesTypeNode(), 8), bytesValueNode('base16', '0102030405060708')), + ), + ], +}); +``` diff --git a/packages/nodes/docs/ProgramNode.md b/packages/nodes/docs/ProgramNode.md index 1e4f33c13..64c4e240d 100644 --- a/packages/nodes/docs/ProgramNode.md +++ b/packages/nodes/docs/ProgramNode.md @@ -1,6 +1,6 @@ # `ProgramNode` -This node represents an entire program deployed on-chain. It defines all elements of a program such as accounts, instructions, PDAs, errors, etc. +This node represents an entire program deployed on-chain. It defines all elements of a program such as accounts, instructions, PDAs, events, errors, etc. ![Diagram](https://github.com/codama-idl/codama/assets/3642397/37ec38ea-66df-4c08-81c3-822ef4388580) @@ -25,6 +25,7 @@ This node represents an entire program deployed on-chain. It defines all element | `instructions` | [`InstructionNode`](./InstructionNode.md)[] | The instructions that allows us to interact with the program. | | `definedTypes` | [`DefinedTypeNode`](./DefinedTypeNode.md)[] | Some reusable types defined by the program. | | `pdas` | [`PdaNode`](./PdaNode.md)[] | The Program-Derived Addresses (PDAs) used by the program. | +| `events` | [`EventNode`](./EventNode.md)[] | The events that can be emitted by the program. | | `errors` | [`ErrorNode`](./ErrorNode.md)[] | The errors that can be thrown by the program. | ## Functions @@ -42,6 +43,7 @@ const node = programNode({ instructions: [], definedTypes: [], pdas: [], + events: [], errors: [], }); ``` @@ -70,6 +72,14 @@ Helper function that returns all `AccountNodes` under a given node. This can be const allAccounts = getAllAccounts(rootNode); ``` +### `getAllEvents(node)` + +Helper function that returns all `EventNodes` under a given node. This can be a `RootNode`, a `ProgramNode` or an array of `ProgramNode`. + +```ts +const allEvents = getAllEvents(rootNode); +``` + ### `getAllDefinedTypes(node)` Helper function that returns all `DefinedTypeNodes` under a given node. This can be a `RootNode`, a `ProgramNode` or an array of `ProgramNode`. diff --git a/packages/nodes/src/EventNode.ts b/packages/nodes/src/EventNode.ts new file mode 100644 index 000000000..0bf54ad5e --- /dev/null +++ b/packages/nodes/src/EventNode.ts @@ -0,0 +1,29 @@ +import type { DiscriminatorNode, EventNode, TypeNode } from '@codama/node-types'; + +import { camelCase, DocsInput, parseDocs } from './shared'; +import { structTypeNode } from './typeNodes'; + +export type EventNodeInput< + TData extends TypeNode = TypeNode, + TDiscriminators extends DiscriminatorNode[] | undefined = DiscriminatorNode[] | undefined, +> = Omit, 'docs' | 'kind' | 'name'> & { + readonly docs?: DocsInput; + readonly name: string; +}; + +export function eventNode< + TData extends TypeNode = ReturnType, + const TDiscriminators extends DiscriminatorNode[] | undefined = undefined, +>(input: EventNodeInput): EventNode { + return Object.freeze({ + kind: 'eventNode', + + // Data. + name: camelCase(input.name), + docs: parseDocs(input.docs), + + // Children. + data: input.data, + ...(input.discriminators !== undefined && { discriminators: input.discriminators }), + }); +} diff --git a/packages/nodes/src/Node.ts b/packages/nodes/src/Node.ts index 73b2a7148..ea7b5aaad 100644 --- a/packages/nodes/src/Node.ts +++ b/packages/nodes/src/Node.ts @@ -22,6 +22,7 @@ export const REGISTERED_NODE_KINDS = [ 'programNode' as const, 'pdaNode' as const, 'accountNode' as const, + 'eventNode' as const, 'instructionAccountNode' as const, 'instructionArgumentNode' as const, 'instructionByteDeltaNode' as const, diff --git a/packages/nodes/src/ProgramNode.ts b/packages/nodes/src/ProgramNode.ts index 1184b6dcb..cad186ed9 100644 --- a/packages/nodes/src/ProgramNode.ts +++ b/packages/nodes/src/ProgramNode.ts @@ -2,6 +2,7 @@ import type { AccountNode, DefinedTypeNode, ErrorNode, + EventNode, InstructionNode, PdaNode, ProgramNode, @@ -16,8 +17,9 @@ export type ProgramNodeInput< TInstructions extends InstructionNode[] = InstructionNode[], TDefinedTypes extends DefinedTypeNode[] = DefinedTypeNode[], TErrors extends ErrorNode[] = ErrorNode[], + TEvents extends EventNode[] = EventNode[], > = Omit< - Partial>, + Partial>, 'docs' | 'kind' | 'name' | 'publicKey' > & { readonly docs?: DocsInput; @@ -31,9 +33,10 @@ export function programNode< const TInstructions extends InstructionNode[] = [], const TDefinedTypes extends DefinedTypeNode[] = [], const TErrors extends ErrorNode[] = [], + const TEvents extends EventNode[] = [], >( - input: ProgramNodeInput, -): ProgramNode { + input: ProgramNodeInput, +): ProgramNode { return Object.freeze({ kind: 'programNode', @@ -49,6 +52,7 @@ export function programNode< instructions: (input.instructions ?? []) as TInstructions, definedTypes: (input.definedTypes ?? []) as TDefinedTypes, pdas: (input.pdas ?? []) as TPdas, + events: (input.events ?? []) as TEvents, errors: (input.errors ?? []) as TErrors, }); } @@ -67,6 +71,10 @@ export function getAllAccounts(node: ProgramNode | ProgramNode[] | RootNode): Ac return getAllPrograms(node).flatMap(program => program.accounts); } +export function getAllEvents(node: ProgramNode | ProgramNode[] | RootNode): EventNode[] { + return getAllPrograms(node).flatMap(program => program.events); +} + export function getAllDefinedTypes(node: ProgramNode | ProgramNode[] | RootNode): DefinedTypeNode[] { return getAllPrograms(node).flatMap(program => program.definedTypes); } diff --git a/packages/nodes/src/index.ts b/packages/nodes/src/index.ts index 39ffa8649..6294d2322 100644 --- a/packages/nodes/src/index.ts +++ b/packages/nodes/src/index.ts @@ -12,6 +12,7 @@ export * from './shared'; export * from './AccountNode'; export * from './DefinedTypeNode'; +export * from './EventNode'; export * from './ErrorNode'; export * from './InstructionAccountNode'; export * from './InstructionArgumentNode'; diff --git a/packages/nodes/test/EventNode.test.ts b/packages/nodes/test/EventNode.test.ts new file mode 100644 index 000000000..532861285 --- /dev/null +++ b/packages/nodes/test/EventNode.test.ts @@ -0,0 +1,14 @@ +import { expect, test } from 'vitest'; + +import { structTypeNode } from '../src'; +import { eventNode } from '../src'; + +test('it returns the right node kind', () => { + const node = eventNode({ data: structTypeNode([]), name: 'foo' }); + expect(node.kind).toBe('eventNode'); +}); + +test('it returns a frozen object', () => { + const node = eventNode({ data: structTypeNode([]), name: 'foo' }); + expect(Object.isFrozen(node)).toBe(true); +}); diff --git a/packages/visitors-core/src/identityVisitor.ts b/packages/visitors-core/src/identityVisitor.ts index a5928b2af..3c8555ec5 100644 --- a/packages/visitors-core/src/identityVisitor.ts +++ b/packages/visitors-core/src/identityVisitor.ts @@ -22,6 +22,7 @@ import { enumTupleVariantTypeNode, enumTypeNode, enumValueNode, + eventNode, fixedSizeTypeNode, hiddenPrefixTypeNode, hiddenSuffixTypeNode, @@ -107,6 +108,7 @@ export function identityVisitor( .map(visit(this)) .filter(removeNullAndAssertIsNodeFilter('definedTypeNode')), errors: node.errors.map(visit(this)).filter(removeNullAndAssertIsNodeFilter('errorNode')), + events: (node.events ?? []).map(visit(this)).filter(removeNullAndAssertIsNodeFilter('eventNode')), instructions: node.instructions .map(visit(this)) .filter(removeNullAndAssertIsNodeFilter('instructionNode')), @@ -135,6 +137,21 @@ export function identityVisitor( }; } + if (keys.includes('eventNode')) { + visitor.visitEvent = function visitEvent(node) { + const data = visit(this)(node.data); + if (data === null) return null; + assertIsNode(data, TYPE_NODES); + return eventNode({ + ...node, + data, + discriminators: node.discriminators + ? node.discriminators.map(visit(this)).filter(removeNullAndAssertIsNodeFilter(DISCRIMINATOR_NODES)) + : undefined, + }); + }; + } + if (keys.includes('instructionNode')) { visitor.visitInstruction = function visitInstruction(node) { const status = node.status ? (visit(this)(node.status) ?? undefined) : undefined; diff --git a/packages/visitors-core/src/mergeVisitor.ts b/packages/visitors-core/src/mergeVisitor.ts index ca8999ba0..b735e528f 100644 --- a/packages/visitors-core/src/mergeVisitor.ts +++ b/packages/visitors-core/src/mergeVisitor.ts @@ -26,6 +26,7 @@ export function mergeVisitor( return merge(node, [ ...node.pdas.flatMap(visit(this)), ...node.accounts.flatMap(visit(this)), + ...(node.events ?? []).flatMap(visit(this)), ...node.instructions.flatMap(visit(this)), ...node.definedTypes.flatMap(visit(this)), ...node.errors.flatMap(visit(this)), @@ -49,6 +50,12 @@ export function mergeVisitor( }; } + if (keys.includes('eventNode')) { + visitor.visitEvent = function visitEvent(node) { + return merge(node, [...visit(this)(node.data), ...(node.discriminators ?? []).flatMap(visit(this))]); + }; + } + if (keys.includes('instructionNode')) { visitor.visitInstruction = function visitInstruction(node) { return merge(node, [ diff --git a/packages/visitors-core/test/nodes/EventNode.test.ts b/packages/visitors-core/test/nodes/EventNode.test.ts new file mode 100644 index 000000000..23d5e191c --- /dev/null +++ b/packages/visitors-core/test/nodes/EventNode.test.ts @@ -0,0 +1,51 @@ +import { + eventNode, + numberTypeNode, + publicKeyTypeNode, + sizeDiscriminatorNode, + structFieldTypeNode, + structTypeNode, +} from '@codama/nodes'; +import { test } from 'vitest'; + +import { + expectDebugStringVisitor, + expectDeleteNodesVisitor, + expectIdentityVisitor, + expectMergeVisitorCount, +} from './_setup'; + +const node = eventNode({ + data: structTypeNode([ + structFieldTypeNode({ name: 'authority', type: publicKeyTypeNode() }), + structFieldTypeNode({ name: 'amount', type: numberTypeNode('u64') }), + ]), + discriminators: [sizeDiscriminatorNode(40)], + name: 'transferEvent', +}); + +test('mergeVisitor', () => { + expectMergeVisitorCount(node, 7); +}); + +test('identityVisitor', () => { + expectIdentityVisitor(node); +}); + +test('deleteNodesVisitor', () => { + expectDeleteNodesVisitor(node, '[eventNode]', null); +}); + +test('debugStringVisitor', () => { + expectDebugStringVisitor( + node, + ` +eventNode [transferEvent] +| structTypeNode +| | structFieldTypeNode [authority] +| | | publicKeyTypeNode +| | structFieldTypeNode [amount] +| | | numberTypeNode [u64] +| sizeDiscriminatorNode [40]`, + ); +}); diff --git a/packages/visitors-core/test/nodes/ProgramNode.test.ts b/packages/visitors-core/test/nodes/ProgramNode.test.ts index 5cd0c5876..7b0c85d98 100644 --- a/packages/visitors-core/test/nodes/ProgramNode.test.ts +++ b/packages/visitors-core/test/nodes/ProgramNode.test.ts @@ -3,6 +3,7 @@ import { definedTypeNode, enumTypeNode, errorNode, + eventNode, instructionNode, pdaNode, programNode, @@ -27,6 +28,7 @@ const node = programNode({ errorNode({ code: 1, message: 'Invalid mint', name: 'invalidMint' }), errorNode({ code: 2, message: 'Invalid token', name: 'invalidToken' }), ], + events: [eventNode({ data: structTypeNode([]), name: 'transferEvent' })], instructions: [instructionNode({ name: 'mintTokens' }), instructionNode({ name: 'transferTokens' })], name: 'splToken', pdas: [pdaNode({ name: 'associatedToken', seeds: [] })], @@ -35,7 +37,7 @@ const node = programNode({ }); test('mergeVisitor', () => { - expectMergeVisitorCount(node, 13); + expectMergeVisitorCount(node, 15); }); test('identityVisitor', () => { @@ -46,6 +48,7 @@ test('deleteNodesVisitor', () => { expectDeleteNodesVisitor(node, '[programNode]', null); expectDeleteNodesVisitor(node, '[pdaNode]', { ...node, pdas: [] }); expectDeleteNodesVisitor(node, '[accountNode]', { ...node, accounts: [] }); + expectDeleteNodesVisitor(node, '[eventNode]', { ...node, events: [] }); expectDeleteNodesVisitor(node, '[instructionNode]', { ...node, instructions: [] }); expectDeleteNodesVisitor(node, '[definedTypeNode]', { ...node, definedTypes: [] }); expectDeleteNodesVisitor(node, '[errorNode]', { ...node, errors: [] }); @@ -61,6 +64,8 @@ programNode [splToken.TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA] | | structTypeNode | accountNode [token] | | structTypeNode +| eventNode [transferEvent] +| | structTypeNode | instructionNode [mintTokens] | instructionNode [transferTokens] | definedTypeNode [tokenState] diff --git a/packages/visitors/README.md b/packages/visitors/README.md index c9b632d63..e6e4c986e 100644 --- a/packages/visitors/README.md +++ b/packages/visitors/README.md @@ -143,13 +143,15 @@ The returned histogram is an object such that the keys are the names of visited ```ts export type DefinedTypeHistogram = { - [key: CamelCaseString]: { + [key: CamelCaseString | `${CamelCaseString}.${CamelCaseString}`]: { // The number of times the type is used as a direct instruction argument. directlyAsInstructionArgs: number; // The number of times the type is used in account data. inAccounts: number; // The number of times the type is used in other defined types. inDefinedTypes: number; + // The number of times the type is used in event payloads. + inEvents: number; // The number of times the type is used in instruction arguments. inInstructionArgs: number; // The number of times the type is used in total. diff --git a/packages/visitors/src/getDefinedTypeHistogramVisitor.ts b/packages/visitors/src/getDefinedTypeHistogramVisitor.ts index 008bb7649..c2fc84f05 100644 --- a/packages/visitors/src/getDefinedTypeHistogramVisitor.ts +++ b/packages/visitors/src/getDefinedTypeHistogramVisitor.ts @@ -18,6 +18,7 @@ export type DefinedTypeHistogram = { directlyAsInstructionArgs: number; inAccounts: number; inDefinedTypes: number; + inEvents: number; inInstructionArgs: number; total: number; }; @@ -35,6 +36,7 @@ function mergeHistograms(histograms: DefinedTypeHistogram[]): DefinedTypeHistogr result[mainCaseKey].total += histogram[mainCaseKey].total; result[mainCaseKey].inAccounts += histogram[mainCaseKey].inAccounts; result[mainCaseKey].inDefinedTypes += histogram[mainCaseKey].inDefinedTypes; + result[mainCaseKey].inEvents += histogram[mainCaseKey].inEvents; result[mainCaseKey].inInstructionArgs += histogram[mainCaseKey].inInstructionArgs; result[mainCaseKey].directlyAsInstructionArgs += histogram[mainCaseKey].directlyAsInstructionArgs; } @@ -46,7 +48,7 @@ function mergeHistograms(histograms: DefinedTypeHistogram[]): DefinedTypeHistogr export function getDefinedTypeHistogramVisitor(): Visitor { const stack = new NodeStack(); - let mode: 'account' | 'definedType' | 'instruction' | null = null; + let mode: 'account' | 'definedType' | 'event' | 'instruction' | null = null; let stackLevel = 0; return pipe( @@ -87,12 +89,21 @@ export function getDefinedTypeHistogramVisitor(): Visitor directlyAsInstructionArgs: Number(mode === 'instruction' && stackLevel <= 1), inAccounts: Number(mode === 'account'), inDefinedTypes: Number(mode === 'definedType'), + inEvents: Number(mode === 'event'), inInstructionArgs: Number(mode === 'instruction'), total: 1, }, }; }, + visitEvent(node, { self }) { + mode = 'event'; + stackLevel = 0; + const histogram = visit(node.data, self); + mode = null; + return histogram; + }, + visitInstruction(node, { self }) { mode = 'instruction'; stackLevel = 0; diff --git a/packages/visitors/test/getDefinedTypeHistogramVisitor.test.ts b/packages/visitors/test/getDefinedTypeHistogramVisitor.test.ts index a429f5444..f9f17b641 100644 --- a/packages/visitors/test/getDefinedTypeHistogramVisitor.test.ts +++ b/packages/visitors/test/getDefinedTypeHistogramVisitor.test.ts @@ -3,6 +3,7 @@ import { definedTypeLinkNode, definedTypeNode, enumTypeNode, + eventNode, instructionArgumentNode, instructionNode, numberTypeNode, @@ -71,6 +72,7 @@ test('it counts the amount of times defined types are used within the tree', () directlyAsInstructionArgs: 0, inAccounts: 1, inDefinedTypes: 0, + inEvents: 0, inInstructionArgs: 0, total: 1, }, @@ -78,12 +80,45 @@ test('it counts the amount of times defined types are used within the tree', () directlyAsInstructionArgs: 1, inAccounts: 1, inDefinedTypes: 0, + inEvents: 0, inInstructionArgs: 1, total: 2, }, }); }); +test('it counts defined types used inside event payloads', () => { + const node = programNode({ + definedTypes: [definedTypeNode({ name: 'eventPayload', type: structTypeNode([]) })], + events: [ + eventNode({ + data: structTypeNode([ + structFieldTypeNode({ + name: 'payload', + type: definedTypeLinkNode('eventPayload'), + }), + ]), + name: 'payloadCreated', + }), + ], + name: 'customProgram', + publicKey: '1111', + }); + + const histogram = visit(node, getDefinedTypeHistogramVisitor()); + + expect(histogram).toEqual({ + 'customProgram.eventPayload': { + directlyAsInstructionArgs: 0, + inAccounts: 0, + inDefinedTypes: 0, + inEvents: 1, + inInstructionArgs: 0, + total: 1, + }, + }); +}); + test('it counts links from different programs separately', () => { // Given a program node with a defined type used in another type. const programA = programNode({ @@ -115,6 +150,7 @@ test('it counts links from different programs separately', () => { directlyAsInstructionArgs: 0, inAccounts: 0, inDefinedTypes: 1, + inEvents: 0, inInstructionArgs: 0, total: 1, }, @@ -122,6 +158,7 @@ test('it counts links from different programs separately', () => { directlyAsInstructionArgs: 0, inAccounts: 0, inDefinedTypes: 1, + inEvents: 0, inInstructionArgs: 0, total: 1, },