From 2c26110f8b5113aee68f984e5fc6d7eeaecfd4c4 Mon Sep 17 00:00:00 2001 From: daog1 Date: Thu, 26 Mar 2026 21:40:42 +0800 Subject: [PATCH 1/5] feat: add event support to dynamic parsers and codecs --- packages/dynamic-codecs/src/codecs.ts | 5 + .../test/codecs/EventNode.test.ts | 21 ++ packages/dynamic-parsers/README.md | 40 ++- .../dynamic-parsers/src/discriminators.ts | 17 +- packages/dynamic-parsers/src/identify.ts | 28 +- packages/dynamic-parsers/src/parsers.ts | 64 ++++- .../dynamic-parsers/test/identify.test.ts | 98 ++++++- packages/dynamic-parsers/test/parsers.test.ts | 242 +++++++++++++++++- 8 files changed, 495 insertions(+), 20 deletions(-) create mode 100644 packages/dynamic-codecs/test/codecs/EventNode.test.ts diff --git a/packages/dynamic-codecs/src/codecs.ts b/packages/dynamic-codecs/src/codecs.ts index 07d83ddde..9da5402a1 100644 --- a/packages/dynamic-codecs/src/codecs.ts +++ b/packages/dynamic-codecs/src/codecs.ts @@ -10,6 +10,7 @@ import { CountNode, DefinedTypeLinkNode, DefinedTypeNode, + EventNode, InstructionArgumentLinkNode, InstructionArgumentNode, InstructionLinkNode, @@ -85,6 +86,7 @@ export type EncodableNodes = | AccountNode | DefinedTypeLinkNode | DefinedTypeNode + | EventNode | InstructionArgumentLinkNode | InstructionArgumentNode | InstructionLinkNode @@ -199,6 +201,9 @@ export function getNodeCodecVisitor( const variants = node.variants.map(variant => [pascalCase(variant.name), visit(variant, this)] as const); return getDiscriminatedUnionCodec(variants, { size }) as unknown as Codec; }, + visitEvent(node) { + return visit(node.data, this); + }, visitFixedSizeType(node) { const type = visit(node.type, this); return fixCodecSize(type, node.size); diff --git a/packages/dynamic-codecs/test/codecs/EventNode.test.ts b/packages/dynamic-codecs/test/codecs/EventNode.test.ts new file mode 100644 index 000000000..65b600a28 --- /dev/null +++ b/packages/dynamic-codecs/test/codecs/EventNode.test.ts @@ -0,0 +1,21 @@ +import { eventNode, numberTypeNode, structFieldTypeNode, structTypeNode } from '@codama/nodes'; +import { expect, test } from 'vitest'; + +import { getNodeCodec } from '../../src'; +import { hex } from '../_setup'; + +test('it delegates to the underlying event data node', () => { + const codec = getNodeCodec([ + eventNode({ + data: structTypeNode([ + structFieldTypeNode({ + name: 'foo', + type: numberTypeNode('u32'), + }), + ]), + name: 'myEvent', + }), + ]); + expect(codec.encode({ foo: 42 })).toStrictEqual(hex('2a000000')); + expect(codec.decode(hex('2a000000'))).toStrictEqual({ foo: 42 }); +}); diff --git a/packages/dynamic-parsers/README.md b/packages/dynamic-parsers/README.md index 70ba166ef..355835a9c 100644 --- a/packages/dynamic-parsers/README.md +++ b/packages/dynamic-parsers/README.md @@ -7,7 +7,7 @@ [npm-image]: https://img.shields.io/npm/v/@codama/dynamic-parsers.svg?style=flat&label=%40codama%2Fdynamic-parsers [npm-url]: https://www.npmjs.com/package/@codama/dynamic-parsers -This package provides a set of helpers that, given any Codama IDL, dynamically identifies and parses any byte array into deserialized accounts and instructions. +This package provides a set of helpers that, given any Codama IDL, dynamically identifies and parses any byte array into deserialized accounts, events, and instructions. ## Installation @@ -25,7 +25,7 @@ pnpm install @codama/dynamic-parsers This type represents the result of identifying and parsing a byte array from a given root node. It provides us with the full `NodePath` of the identified node, as well as the data deserialized from the provided bytes. ```ts -type ParsedData = { +type ParsedData = { data: unknown; path: NodePath; }; @@ -47,6 +47,20 @@ if (parsedData) { } ``` +### `parseEventData(rootNode, bytes)` + +Similarly to `parseAccountData`, this function will match the provided bytes to an event node and deserialize them accordingly. It returns a `ParsedData` object if the parsing was successful, or `undefined` otherwise. + +```ts +const parsedData = parseEventData(rootNode, bytes); +// ^ ParsedData | undefined + +if (parsedData) { + const eventNode: EventNode = getLastNodeFromPath(parsedData.path); + const decodedData: unknown = parsedData.data; +} +``` + ### `parseInstructionData(rootNode, bytes)` Similarly to `parseAccountData`, this function will match the provided bytes to an instruction node and deserialize them accordingly. It returns a `ParsedData` object if the parsing was successful, or `undefined` otherwise. @@ -74,6 +88,15 @@ if (parsedData) { } ``` +### `parseDataByName(rootNode, bytes, name, kind?)` + +This function bypasses discriminator-based identification and decodes the provided bytes using a node selected by name from the root program. It can target accounts, events, instructions, or any combination of those kinds. + +```ts +const parsedData = parseDataByName(rootNode, bytes, 'myEvent', 'eventNode'); +// ^ ParsedData | undefined +``` + ### `identifyAccountData` This function tries to match the provided bytes to an account node, returning a `NodePath` object if the identification was successful, or `undefined` otherwise. It is used by the `parseAccountData` function under the hood. @@ -99,3 +122,16 @@ if (path) { const instructionNode: InstructionNode = getLastNodeFromPath(path); } ``` + +### `identifyEventData` + +This function tries to match the provided bytes to an event node, returning a `NodePath` object if the identification was successful, or `undefined` otherwise. It is used by the `parseEventData` function under the hood. + +```ts +const path = identifyEventData(root, bytes); +// ^ NodePath | undefined + +if (path) { + const eventNode: EventNode = getLastNodeFromPath(path); +} +``` diff --git a/packages/dynamic-parsers/src/discriminators.ts b/packages/dynamic-parsers/src/discriminators.ts index 1038ff0f3..13bf8299e 100644 --- a/packages/dynamic-parsers/src/discriminators.ts +++ b/packages/dynamic-parsers/src/discriminators.ts @@ -14,32 +14,33 @@ import { isNode, SizeDiscriminatorNode, StructTypeNode, + TypeNode, } from '@codama/nodes'; import { visit } from '@codama/visitors-core'; export function matchDiscriminators( bytes: ReadonlyUint8Array, discriminators: DiscriminatorNode[], - struct: StructTypeNode, + typeNode: TypeNode, visitors: CodecAndValueVisitors, ): boolean { return ( discriminators.length > 0 && - discriminators.every(discriminator => matchDiscriminator(bytes, discriminator, struct, visitors)) + discriminators.every(discriminator => matchDiscriminator(bytes, discriminator, typeNode, visitors)) ); } function matchDiscriminator( bytes: ReadonlyUint8Array, discriminator: DiscriminatorNode, - struct: StructTypeNode, + typeNode: TypeNode, visitors: CodecAndValueVisitors, ): boolean { if (isNode(discriminator, 'constantDiscriminatorNode')) { return matchConstantDiscriminator(bytes, discriminator, visitors); } if (isNode(discriminator, 'fieldDiscriminatorNode')) { - return matchFieldDiscriminator(bytes, discriminator, struct, visitors); + return matchFieldDiscriminator(bytes, discriminator, typeNode, visitors); } assertIsNode(discriminator, 'sizeDiscriminatorNode'); return matchSizeDiscriminator(bytes, discriminator); @@ -59,9 +60,15 @@ function matchConstantDiscriminator( function matchFieldDiscriminator( bytes: ReadonlyUint8Array, discriminator: FieldDiscriminatorNode, - struct: StructTypeNode, + typeNode: TypeNode, visitors: CodecAndValueVisitors, ): boolean { + if (!isNode(typeNode, 'structTypeNode')) { + throw new CodamaError(CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND, { + field: discriminator.name, + }); + } + const struct = typeNode as StructTypeNode; const field = struct.fields.find(field => field.name === discriminator.name); if (!field) { throw new CodamaError(CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND, { diff --git a/packages/dynamic-parsers/src/identify.ts b/packages/dynamic-parsers/src/identify.ts index 14757a121..4dc0906e0 100644 --- a/packages/dynamic-parsers/src/identify.ts +++ b/packages/dynamic-parsers/src/identify.ts @@ -1,6 +1,7 @@ import { CodecAndValueVisitors, getCodecAndValueVisitors, ReadonlyUint8Array } from '@codama/dynamic-codecs'; import { AccountNode, + EventNode, GetNodeFromKind, InstructionNode, isNodeFilter, @@ -28,6 +29,13 @@ export function identifyAccountData( return identifyData(root, bytes, 'accountNode'); } +export function identifyEventData( + root: RootNode, + bytes: ReadonlyUint8Array | Uint8Array, +): NodePath | undefined { + return identifyData(root, bytes, 'eventNode'); +} + export function identifyInstructionData( root: RootNode, bytes: ReadonlyUint8Array | Uint8Array, @@ -35,7 +43,7 @@ export function identifyInstructionData( return identifyData(root, bytes, 'instructionNode'); } -export function identifyData( +export function identifyData( root: RootNode, bytes: ReadonlyUint8Array | Uint8Array, kind?: TKind | TKind[], @@ -46,7 +54,7 @@ export function identifyData( const codecAndValueVisitors = getCodecAndValueVisitors(linkables, { stack }); const visitor = getByteIdentificationVisitor( - kind ?? (['accountNode', 'instructionNode'] as TKind[]), + kind ?? (['accountNode', 'instructionNode', 'eventNode'] as TKind[]), bytes, codecAndValueVisitors, { stack }, @@ -55,7 +63,7 @@ export function identifyData( return visit(root, visitor); } -export function getByteIdentificationVisitor( +export function getByteIdentificationVisitor( kind: TKind | TKind[], bytes: ReadonlyUint8Array | Uint8Array, codecAndValueVisitors: CodecAndValueVisitors, @@ -71,6 +79,16 @@ export function getByteIdentificationVisitor> | undefined, - 'accountNode' | 'instructionNode' | 'programNode' | 'rootNode' + 'accountNode' | 'eventNode' | 'instructionNode' | 'programNode' | 'rootNode' >, v => recordNodeStackVisitor(v, stack), ); diff --git a/packages/dynamic-parsers/src/parsers.ts b/packages/dynamic-parsers/src/parsers.ts index 13c0f77b3..df64d52b5 100644 --- a/packages/dynamic-parsers/src/parsers.ts +++ b/packages/dynamic-parsers/src/parsers.ts @@ -1,5 +1,5 @@ -import { getNodeCodec, ReadonlyUint8Array } from '@codama/dynamic-codecs'; -import { AccountNode, CamelCaseString, GetNodeFromKind, InstructionNode, RootNode } from '@codama/nodes'; +import { type EncodableNodes, getNodeCodec, ReadonlyUint8Array } from '@codama/dynamic-codecs'; +import { AccountNode, CamelCaseString, EventNode, GetNodeFromKind, InstructionNode, RootNode } from '@codama/nodes'; import { getLastNodeFromPath, NodePath } from '@codama/visitors-core'; import type { AccountLookupMeta, @@ -11,11 +11,39 @@ import type { import { identifyData } from './identify'; -export type ParsedData = { +type ParsableNode = Extract; +type ParsableNodeKind = ParsableNode['kind']; + +export type ParsedData = { data: unknown; path: NodePath; }; +function findPathByName( + root: RootNode, + name: string, + kind: TKind | readonly TKind[], +): NodePath> | undefined { + const kinds = new Set(Array.isArray(kind) ? kind : [kind]); + + if (kinds.has('accountNode' as TKind)) { + const account = root.program.accounts.find(candidate => candidate.name === name); + if (account) return [root, root.program, account] as unknown as NodePath>; + } + + if (kinds.has('eventNode' as TKind)) { + const event = root.program.events.find(candidate => candidate.name === name); + if (event) return [root, root.program, event] as unknown as NodePath>; + } + + if (kinds.has('instructionNode' as TKind)) { + const instruction = root.program.instructions.find(candidate => candidate.name === name); + if (instruction) return [root, root.program, instruction] as unknown as NodePath>; + } + + return undefined; +} + export function parseAccountData( root: RootNode, bytes: ReadonlyUint8Array | Uint8Array, @@ -23,6 +51,13 @@ export function parseAccountData( return parseData(root, bytes, 'accountNode'); } +export function parseEventData( + root: RootNode, + bytes: ReadonlyUint8Array | Uint8Array, +): ParsedData | undefined { + return parseData(root, bytes, 'eventNode'); +} + export function parseInstructionData( root: RootNode, bytes: ReadonlyUint8Array | Uint8Array, @@ -30,14 +65,31 @@ export function parseInstructionData( return parseData(root, bytes, 'instructionNode'); } -export function parseData( +export function parseData( + root: RootNode, + bytes: ReadonlyUint8Array | Uint8Array, + kind?: TKind | TKind[], +): ParsedData> | undefined { + const path = identifyData(root, bytes, kind ?? (['accountNode', 'instructionNode', 'eventNode'] as TKind[])); + if (!path) return undefined; + const codec = getNodeCodec(path as NodePath); + const data = codec.decode(bytes); + return { data, path }; +} + +export function parseDataByName( root: RootNode, bytes: ReadonlyUint8Array | Uint8Array, + name: string, kind?: TKind | TKind[], ): ParsedData> | undefined { - const path = identifyData(root, bytes, kind ?? (['accountNode', 'instructionNode'] as TKind[])); + const path = findPathByName( + root, + name, + kind ?? (['accountNode', 'instructionNode', 'eventNode'] as TKind[]), + ); if (!path) return undefined; - const codec = getNodeCodec(path as NodePath); + const codec = getNodeCodec(path as NodePath); const data = codec.decode(bytes); return { data, path }; } diff --git a/packages/dynamic-parsers/test/identify.test.ts b/packages/dynamic-parsers/test/identify.test.ts index 8c0b3bca0..290154f04 100644 --- a/packages/dynamic-parsers/test/identify.test.ts +++ b/packages/dynamic-parsers/test/identify.test.ts @@ -1,15 +1,23 @@ import { accountNode, + bytesTypeNode, constantDiscriminatorNode, + constantValueNode, constantValueNodeFromBytes, + eventNode, + fixedSizeTypeNode, + hiddenPrefixTypeNode, instructionNode, + numberTypeNode, programNode, rootNode, sizeDiscriminatorNode, + structTypeNode, + tupleTypeNode, } from '@codama/nodes'; import { describe, expect, test } from 'vitest'; -import { identifyAccountData, identifyInstructionData } from '../src'; +import { identifyAccountData, identifyEventData, identifyInstructionData } from '../src'; import { hex } from './_setup'; describe('identifyAccountData', () => { @@ -165,3 +173,91 @@ describe('identifyInstructionData', () => { expect(result).toBeUndefined(); }); }); + +describe('identifyEventData', () => { + test('it identifies an event using its discriminator nodes', () => { + const root = rootNode( + programNode({ + events: [ + eventNode({ + data: structTypeNode([]), + discriminators: [sizeDiscriminatorNode(4)], + name: 'myEvent', + }), + ], + name: 'myProgram', + publicKey: '1111', + }), + ); + const result = identifyEventData(root, hex('01020304')); + expect(result).toStrictEqual([root, root.program, root.program.events[0]]); + }); + test('it fails to identify events whose discriminator nodes do not match the given data', () => { + const root = rootNode( + programNode({ + events: [ + eventNode({ + data: structTypeNode([]), + discriminators: [sizeDiscriminatorNode(999)], + name: 'myEvent', + }), + ], + name: 'myProgram', + publicKey: '1111', + }), + ); + const result = identifyEventData(root, hex('01020304')); + expect(result).toBeUndefined(); + }); + test('it fails to identify events with no discriminator nodes', () => { + const root = rootNode( + programNode({ + events: [eventNode({ data: structTypeNode([]), name: 'myEvent' })], + name: 'myProgram', + publicKey: '1111', + }), + ); + const result = identifyEventData(root, hex('01020304')); + expect(result).toBeUndefined(); + }); + test('it does not identify events using instruction discriminators', () => { + const root = rootNode( + programNode({ + instructions: [instructionNode({ discriminators: [sizeDiscriminatorNode(4)], name: 'myInstruction' })], + name: 'myProgram', + publicKey: '1111', + }), + ); + const result = identifyEventData(root, hex('01020304')); + expect(result).toBeUndefined(); + }); + test('it identifies tuple events using constant discriminators', () => { + const root = rootNode( + programNode({ + events: [ + eventNode({ + data: hiddenPrefixTypeNode(tupleTypeNode([numberTypeNode('u32')]), [ + constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), 2), + constantValueNodeFromBytes('base16', '0102'), + ), + ]), + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), 2), + constantValueNodeFromBytes('base16', '0102'), + ), + ), + ], + name: 'tupleEvent', + }), + ], + name: 'myProgram', + publicKey: '1111', + }), + ); + const result = identifyEventData(root, hex('01022a000000')); + expect(result).toStrictEqual([root, root.program, root.program.events[0]]); + }); +}); diff --git a/packages/dynamic-parsers/test/parsers.test.ts b/packages/dynamic-parsers/test/parsers.test.ts index 266bcf06a..09314724b 100644 --- a/packages/dynamic-parsers/test/parsers.test.ts +++ b/packages/dynamic-parsers/test/parsers.test.ts @@ -1,6 +1,13 @@ import { accountNode, + bytesTypeNode, + constantDiscriminatorNode, + constantValueNode, + constantValueNodeFromBytes, + eventNode, fieldDiscriminatorNode, + fixedSizeTypeNode, + hiddenPrefixTypeNode, instructionArgumentNode, instructionNode, numberTypeNode, @@ -11,10 +18,11 @@ import { stringTypeNode, structFieldTypeNode, structTypeNode, + tupleTypeNode, } from '@codama/nodes'; import { describe, expect, test } from 'vitest'; -import { parseAccountData, parseInstructionData } from '../src'; +import { parseAccountData, parseDataByName, parseEventData, parseInstructionData } from '../src'; import { hex } from './_setup'; describe('parseAccountData', () => { @@ -90,3 +98,235 @@ describe('parseInstructionData', () => { }); }); }); + +describe('parseEventData', () => { + test('it parses some event data from a root node', () => { + const root = rootNode( + programNode({ + events: [ + eventNode({ + data: structTypeNode([ + structFieldTypeNode({ + defaultValue: numberValueNode(9), + name: 'discriminator', + type: numberTypeNode('u8'), + }), + structFieldTypeNode({ + name: 'firstname', + type: sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u16')), + }), + structFieldTypeNode({ + name: 'age', + type: numberTypeNode('u8'), + }), + ]), + discriminators: [fieldDiscriminatorNode('discriminator')], + name: 'myEvent', + }), + ], + name: 'myProgram', + publicKey: '1111', + }), + ); + const result = parseEventData(root, hex('090500416c6963652a')); + expect(result).toStrictEqual({ + data: { age: 42, discriminator: 9, firstname: 'Alice' }, + path: [root, root.program, root.program.events[0]], + }); + }); + test('it parses tuple event data from a root node', () => { + const root = rootNode( + programNode({ + events: [ + eventNode({ + data: hiddenPrefixTypeNode(tupleTypeNode([numberTypeNode('u32')]), [ + constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), 2), + constantValueNodeFromBytes('base16', '0102'), + ), + ]), + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), 2), + constantValueNodeFromBytes('base16', '0102'), + ), + ), + ], + name: 'tupleEvent', + }), + ], + name: 'myProgram', + publicKey: '1111', + }), + ); + const result = parseEventData(root, hex('01022a000000')); + expect(result).toStrictEqual({ + data: [42], + path: [root, root.program, root.program.events[0]], + }); + }); +}); + +describe('parseDataByName', () => { + test('it parses account data by name', () => { + const root = rootNode( + programNode({ + accounts: [ + accountNode({ + data: structTypeNode([ + structFieldTypeNode({ + defaultValue: numberValueNode(9), + name: 'discriminator', + type: numberTypeNode('u8'), + }), + structFieldTypeNode({ + name: 'firstname', + type: sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u16')), + }), + structFieldTypeNode({ + name: 'age', + type: numberTypeNode('u8'), + }), + ]), + discriminators: [fieldDiscriminatorNode('discriminator')], + name: 'myAccount', + }), + ], + name: 'myProgram', + publicKey: '1111', + }), + ); + const result = parseDataByName(root, hex('090500416c6963652a'), 'myAccount', 'accountNode'); + expect(result).toStrictEqual({ + data: { age: 42, discriminator: 9, firstname: 'Alice' }, + path: [root, root.program, root.program.accounts[0]], + }); + }); + + test('it parses event data by name', () => { + const root = rootNode( + programNode({ + events: [ + eventNode({ + data: structTypeNode([ + structFieldTypeNode({ + defaultValue: numberValueNode(9), + name: 'discriminator', + type: numberTypeNode('u8'), + }), + structFieldTypeNode({ + name: 'firstname', + type: sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u16')), + }), + structFieldTypeNode({ + name: 'age', + type: numberTypeNode('u8'), + }), + ]), + discriminators: [fieldDiscriminatorNode('discriminator')], + name: 'myEvent', + }), + ], + name: 'myProgram', + publicKey: '1111', + }), + ); + const result = parseDataByName(root, hex('090500416c6963652a'), 'myEvent', 'eventNode'); + expect(result).toStrictEqual({ + data: { age: 42, discriminator: 9, firstname: 'Alice' }, + path: [root, root.program, root.program.events[0]], + }); + }); + test('it parses tuple event data by name', () => { + const root = rootNode( + programNode({ + events: [ + eventNode({ + data: hiddenPrefixTypeNode(tupleTypeNode([numberTypeNode('u32')]), [ + constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), 2), + constantValueNodeFromBytes('base16', '0102'), + ), + ]), + discriminators: [ + constantDiscriminatorNode( + constantValueNode( + fixedSizeTypeNode(bytesTypeNode(), 2), + constantValueNodeFromBytes('base16', '0102'), + ), + ), + ], + name: 'tupleEvent', + }), + ], + name: 'myProgram', + publicKey: '1111', + }), + ); + const result = parseDataByName(root, hex('01022a000000'), 'tupleEvent', 'eventNode'); + expect(result).toStrictEqual({ + data: [42], + path: [root, root.program, root.program.events[0]], + }); + }); + + test('it parses instruction data by name', () => { + const root = rootNode( + programNode({ + instructions: [ + instructionNode({ + arguments: [ + instructionArgumentNode({ + defaultValue: numberValueNode(9), + name: 'discriminator', + type: numberTypeNode('u8'), + }), + instructionArgumentNode({ + name: 'firstname', + type: sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u16')), + }), + instructionArgumentNode({ + name: 'age', + type: numberTypeNode('u8'), + }), + ], + discriminators: [fieldDiscriminatorNode('discriminator')], + name: 'myInstruction', + }), + ], + name: 'myProgram', + publicKey: '1111', + }), + ); + const result = parseDataByName(root, hex('090500416c6963652a'), 'myInstruction', 'instructionNode'); + expect(result).toStrictEqual({ + data: { age: 42, discriminator: 9, firstname: 'Alice' }, + path: [root, root.program, root.program.instructions[0]], + }); + }); + + test('it returns undefined for non-existent names', () => { + const root = rootNode( + programNode({ + accounts: [ + accountNode({ + data: structTypeNode([ + structFieldTypeNode({ + defaultValue: numberValueNode(9), + name: 'discriminator', + type: numberTypeNode('u8'), + }), + ]), + discriminators: [fieldDiscriminatorNode('discriminator')], + name: 'myAccount', + }), + ], + name: 'myProgram', + publicKey: '1111', + }), + ); + const result = parseDataByName(root, hex('09'), 'unknownNode'); + expect(result).toBeUndefined(); + }); +}); From db81016833266850ab99a97a08eb0581a410ee64 Mon Sep 17 00:00:00 2001 From: xiaodao Date: Mon, 30 Mar 2026 22:06:50 +0800 Subject: [PATCH 2/5] chore: add changeset for dynamic event parsing --- .changeset/four-spoons-fix.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 .changeset/four-spoons-fix.md diff --git a/.changeset/four-spoons-fix.md b/.changeset/four-spoons-fix.md new file mode 100644 index 000000000..76888cafd --- /dev/null +++ b/.changeset/four-spoons-fix.md @@ -0,0 +1,6 @@ +--- +"@codama/dynamic-codecs": minor +"@codama/dynamic-parsers": minor +--- + +Add event support to dynamic codecs and parsers. From ba113f91c71550e961c41899ebf602fb18afc44c Mon Sep 17 00:00:00 2001 From: xiaodao Date: Tue, 31 Mar 2026 23:29:41 +0800 Subject: [PATCH 3/5] refactor: remove parseDataByName and fix event codec paths --- packages/dynamic-codecs/src/codecs.ts | 12 +- packages/dynamic-parsers/README.md | 9 - packages/dynamic-parsers/src/parsers.ts | 46 +---- packages/dynamic-parsers/test/parsers.test.ts | 165 +----------------- 4 files changed, 13 insertions(+), 219 deletions(-) diff --git a/packages/dynamic-codecs/src/codecs.ts b/packages/dynamic-codecs/src/codecs.ts index 9da5402a1..e1c1e957b 100644 --- a/packages/dynamic-codecs/src/codecs.ts +++ b/packages/dynamic-codecs/src/codecs.ts @@ -11,6 +11,7 @@ import { DefinedTypeLinkNode, DefinedTypeNode, EventNode, + getAllPrograms, InstructionArgumentLinkNode, InstructionArgumentNode, InstructionLinkNode, @@ -97,9 +98,16 @@ export type CodecVisitorOptions = { bytesEncoding?: BytesEncoding; }; -export function getNodeCodec(path: NodePath, options: CodecVisitorOptions = {}): Codec { +export function getNodeCodec( + path: NodePath, + options: CodecVisitorOptions = {}, +): Codec { const linkables = new LinkableDictionary(); - visit(path[0], getRecordLinkablesVisitor(linkables)); + if (isNode(path[0], 'rootNode')) { + getAllPrograms(path[0]).forEach(program => visit(program, getRecordLinkablesVisitor(linkables))); + } else { + visit(path[0], getRecordLinkablesVisitor(linkables)); + } return visit( getLastNodeFromPath(path), diff --git a/packages/dynamic-parsers/README.md b/packages/dynamic-parsers/README.md index 355835a9c..321566271 100644 --- a/packages/dynamic-parsers/README.md +++ b/packages/dynamic-parsers/README.md @@ -88,15 +88,6 @@ if (parsedData) { } ``` -### `parseDataByName(rootNode, bytes, name, kind?)` - -This function bypasses discriminator-based identification and decodes the provided bytes using a node selected by name from the root program. It can target accounts, events, instructions, or any combination of those kinds. - -```ts -const parsedData = parseDataByName(rootNode, bytes, 'myEvent', 'eventNode'); -// ^ ParsedData | undefined -``` - ### `identifyAccountData` This function tries to match the provided bytes to an account node, returning a `NodePath` object if the identification was successful, or `undefined` otherwise. It is used by the `parseAccountData` function under the hood. diff --git a/packages/dynamic-parsers/src/parsers.ts b/packages/dynamic-parsers/src/parsers.ts index df64d52b5..b50a63728 100644 --- a/packages/dynamic-parsers/src/parsers.ts +++ b/packages/dynamic-parsers/src/parsers.ts @@ -1,4 +1,4 @@ -import { type EncodableNodes, getNodeCodec, ReadonlyUint8Array } from '@codama/dynamic-codecs'; +import { getNodeCodec, ReadonlyUint8Array } from '@codama/dynamic-codecs'; import { AccountNode, CamelCaseString, EventNode, GetNodeFromKind, InstructionNode, RootNode } from '@codama/nodes'; import { getLastNodeFromPath, NodePath } from '@codama/visitors-core'; import type { @@ -11,7 +11,7 @@ import type { import { identifyData } from './identify'; -type ParsableNode = Extract; +type ParsableNode = AccountNode | EventNode | InstructionNode; type ParsableNodeKind = ParsableNode['kind']; export type ParsedData = { @@ -19,31 +19,6 @@ export type ParsedData = { path: NodePath; }; -function findPathByName( - root: RootNode, - name: string, - kind: TKind | readonly TKind[], -): NodePath> | undefined { - const kinds = new Set(Array.isArray(kind) ? kind : [kind]); - - if (kinds.has('accountNode' as TKind)) { - const account = root.program.accounts.find(candidate => candidate.name === name); - if (account) return [root, root.program, account] as unknown as NodePath>; - } - - if (kinds.has('eventNode' as TKind)) { - const event = root.program.events.find(candidate => candidate.name === name); - if (event) return [root, root.program, event] as unknown as NodePath>; - } - - if (kinds.has('instructionNode' as TKind)) { - const instruction = root.program.instructions.find(candidate => candidate.name === name); - if (instruction) return [root, root.program, instruction] as unknown as NodePath>; - } - - return undefined; -} - export function parseAccountData( root: RootNode, bytes: ReadonlyUint8Array | Uint8Array, @@ -77,23 +52,6 @@ export function parseData( return { data, path }; } -export function parseDataByName( - root: RootNode, - bytes: ReadonlyUint8Array | Uint8Array, - name: string, - kind?: TKind | TKind[], -): ParsedData> | undefined { - const path = findPathByName( - root, - name, - kind ?? (['accountNode', 'instructionNode', 'eventNode'] as TKind[]), - ); - if (!path) return undefined; - const codec = getNodeCodec(path as NodePath); - const data = codec.decode(bytes); - return { data, path }; -} - type ParsedInstructionAccounts = ReadonlyArray; type ParsedInstruction = ParsedData & { accounts: ParsedInstructionAccounts }; diff --git a/packages/dynamic-parsers/test/parsers.test.ts b/packages/dynamic-parsers/test/parsers.test.ts index 09314724b..496e16cdb 100644 --- a/packages/dynamic-parsers/test/parsers.test.ts +++ b/packages/dynamic-parsers/test/parsers.test.ts @@ -22,7 +22,7 @@ import { } from '@codama/nodes'; import { describe, expect, test } from 'vitest'; -import { parseAccountData, parseDataByName, parseEventData, parseInstructionData } from '../src'; +import { parseAccountData, parseEventData, parseInstructionData } from '../src'; import { hex } from './_setup'; describe('parseAccountData', () => { @@ -167,166 +167,3 @@ describe('parseEventData', () => { }); }); }); - -describe('parseDataByName', () => { - test('it parses account data by name', () => { - const root = rootNode( - programNode({ - accounts: [ - accountNode({ - data: structTypeNode([ - structFieldTypeNode({ - defaultValue: numberValueNode(9), - name: 'discriminator', - type: numberTypeNode('u8'), - }), - structFieldTypeNode({ - name: 'firstname', - type: sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u16')), - }), - structFieldTypeNode({ - name: 'age', - type: numberTypeNode('u8'), - }), - ]), - discriminators: [fieldDiscriminatorNode('discriminator')], - name: 'myAccount', - }), - ], - name: 'myProgram', - publicKey: '1111', - }), - ); - const result = parseDataByName(root, hex('090500416c6963652a'), 'myAccount', 'accountNode'); - expect(result).toStrictEqual({ - data: { age: 42, discriminator: 9, firstname: 'Alice' }, - path: [root, root.program, root.program.accounts[0]], - }); - }); - - test('it parses event data by name', () => { - const root = rootNode( - programNode({ - events: [ - eventNode({ - data: structTypeNode([ - structFieldTypeNode({ - defaultValue: numberValueNode(9), - name: 'discriminator', - type: numberTypeNode('u8'), - }), - structFieldTypeNode({ - name: 'firstname', - type: sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u16')), - }), - structFieldTypeNode({ - name: 'age', - type: numberTypeNode('u8'), - }), - ]), - discriminators: [fieldDiscriminatorNode('discriminator')], - name: 'myEvent', - }), - ], - name: 'myProgram', - publicKey: '1111', - }), - ); - const result = parseDataByName(root, hex('090500416c6963652a'), 'myEvent', 'eventNode'); - expect(result).toStrictEqual({ - data: { age: 42, discriminator: 9, firstname: 'Alice' }, - path: [root, root.program, root.program.events[0]], - }); - }); - test('it parses tuple event data by name', () => { - const root = rootNode( - programNode({ - events: [ - eventNode({ - data: hiddenPrefixTypeNode(tupleTypeNode([numberTypeNode('u32')]), [ - constantValueNode( - fixedSizeTypeNode(bytesTypeNode(), 2), - constantValueNodeFromBytes('base16', '0102'), - ), - ]), - discriminators: [ - constantDiscriminatorNode( - constantValueNode( - fixedSizeTypeNode(bytesTypeNode(), 2), - constantValueNodeFromBytes('base16', '0102'), - ), - ), - ], - name: 'tupleEvent', - }), - ], - name: 'myProgram', - publicKey: '1111', - }), - ); - const result = parseDataByName(root, hex('01022a000000'), 'tupleEvent', 'eventNode'); - expect(result).toStrictEqual({ - data: [42], - path: [root, root.program, root.program.events[0]], - }); - }); - - test('it parses instruction data by name', () => { - const root = rootNode( - programNode({ - instructions: [ - instructionNode({ - arguments: [ - instructionArgumentNode({ - defaultValue: numberValueNode(9), - name: 'discriminator', - type: numberTypeNode('u8'), - }), - instructionArgumentNode({ - name: 'firstname', - type: sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u16')), - }), - instructionArgumentNode({ - name: 'age', - type: numberTypeNode('u8'), - }), - ], - discriminators: [fieldDiscriminatorNode('discriminator')], - name: 'myInstruction', - }), - ], - name: 'myProgram', - publicKey: '1111', - }), - ); - const result = parseDataByName(root, hex('090500416c6963652a'), 'myInstruction', 'instructionNode'); - expect(result).toStrictEqual({ - data: { age: 42, discriminator: 9, firstname: 'Alice' }, - path: [root, root.program, root.program.instructions[0]], - }); - }); - - test('it returns undefined for non-existent names', () => { - const root = rootNode( - programNode({ - accounts: [ - accountNode({ - data: structTypeNode([ - structFieldTypeNode({ - defaultValue: numberValueNode(9), - name: 'discriminator', - type: numberTypeNode('u8'), - }), - ]), - discriminators: [fieldDiscriminatorNode('discriminator')], - name: 'myAccount', - }), - ], - name: 'myProgram', - publicKey: '1111', - }), - ); - const result = parseDataByName(root, hex('09'), 'unknownNode'); - expect(result).toBeUndefined(); - }); -}); From 7a2d9cbb72f53e1eeefa76e3094c500e07154cc0 Mon Sep 17 00:00:00 2001 From: xiaodao Date: Tue, 31 Mar 2026 23:52:09 +0800 Subject: [PATCH 4/5] refactor: remove unnecessary getNodeCodec changes --- packages/dynamic-codecs/src/codecs.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/packages/dynamic-codecs/src/codecs.ts b/packages/dynamic-codecs/src/codecs.ts index e1c1e957b..28aec5c45 100644 --- a/packages/dynamic-codecs/src/codecs.ts +++ b/packages/dynamic-codecs/src/codecs.ts @@ -11,7 +11,6 @@ import { DefinedTypeLinkNode, DefinedTypeNode, EventNode, - getAllPrograms, InstructionArgumentLinkNode, InstructionArgumentNode, InstructionLinkNode, @@ -103,11 +102,7 @@ export function getNodeCodec( options: CodecVisitorOptions = {}, ): Codec { const linkables = new LinkableDictionary(); - if (isNode(path[0], 'rootNode')) { - getAllPrograms(path[0]).forEach(program => visit(program, getRecordLinkablesVisitor(linkables))); - } else { - visit(path[0], getRecordLinkablesVisitor(linkables)); - } + visit(path[0], getRecordLinkablesVisitor(linkables)); return visit( getLastNodeFromPath(path), From 6ac4e23fced3a93f31dd13bfca02d95a39345e89 Mon Sep 17 00:00:00 2001 From: xiaodao Date: Wed, 1 Apr 2026 08:28:12 +0800 Subject: [PATCH 5/5] refactor: revert unnecessary getNodeCodec signature change --- packages/dynamic-codecs/src/codecs.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/dynamic-codecs/src/codecs.ts b/packages/dynamic-codecs/src/codecs.ts index 28aec5c45..9da5402a1 100644 --- a/packages/dynamic-codecs/src/codecs.ts +++ b/packages/dynamic-codecs/src/codecs.ts @@ -97,10 +97,7 @@ export type CodecVisitorOptions = { bytesEncoding?: BytesEncoding; }; -export function getNodeCodec( - path: NodePath, - options: CodecVisitorOptions = {}, -): Codec { +export function getNodeCodec(path: NodePath, options: CodecVisitorOptions = {}): Codec { const linkables = new LinkableDictionary(); visit(path[0], getRecordLinkablesVisitor(linkables));