Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/four-spoons-fix.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@codama/dynamic-codecs": minor
"@codama/dynamic-parsers": minor
---

Add event support to dynamic codecs and parsers.
5 changes: 5 additions & 0 deletions packages/dynamic-codecs/src/codecs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
CountNode,
DefinedTypeLinkNode,
DefinedTypeNode,
EventNode,
InstructionArgumentLinkNode,
InstructionArgumentNode,
InstructionLinkNode,
Expand Down Expand Up @@ -85,6 +86,7 @@ export type EncodableNodes =
| AccountNode
| DefinedTypeLinkNode
| DefinedTypeNode
| EventNode
| InstructionArgumentLinkNode
| InstructionArgumentNode
| InstructionLinkNode
Expand Down Expand Up @@ -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<unknown>;
},
visitEvent(node) {
return visit(node.data, this);
},
visitFixedSizeType(node) {
const type = visit(node.type, this);
return fixCodecSize(type, node.size);
Expand Down
21 changes: 21 additions & 0 deletions packages/dynamic-codecs/test/codecs/EventNode.test.ts
Original file line number Diff line number Diff line change
@@ -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 });
});
31 changes: 29 additions & 2 deletions packages/dynamic-parsers/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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<TNode extends AccountNode | InstructionNode> = {
type ParsedData<TNode extends AccountNode | EventNode | InstructionNode> = {
data: unknown;
path: NodePath<TNode>;
};
Expand All @@ -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<EventNode>` object if the parsing was successful, or `undefined` otherwise.

```ts
const parsedData = parseEventData(rootNode, bytes);
// ^ ParsedData<EventNode> | 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<InstructionNode>` object if the parsing was successful, or `undefined` otherwise.
Expand Down Expand Up @@ -99,3 +113,16 @@ if (path) {
const instructionNode: InstructionNode = getLastNodeFromPath(path);
}
```

### `identifyEventData`

This function tries to match the provided bytes to an event node, returning a `NodePath<EventNode>` 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<EventNode> | undefined

if (path) {
const eventNode: EventNode = getLastNodeFromPath(path);
}
```
17 changes: 12 additions & 5 deletions packages/dynamic-parsers/src/discriminators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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, {
Expand Down
28 changes: 23 additions & 5 deletions packages/dynamic-parsers/src/identify.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { CodecAndValueVisitors, getCodecAndValueVisitors, ReadonlyUint8Array } from '@codama/dynamic-codecs';
import {
AccountNode,
EventNode,
GetNodeFromKind,
InstructionNode,
isNodeFilter,
Expand Down Expand Up @@ -28,14 +29,21 @@ export function identifyAccountData(
return identifyData(root, bytes, 'accountNode');
}

export function identifyEventData(
root: RootNode,
bytes: ReadonlyUint8Array | Uint8Array,
): NodePath<EventNode> | undefined {
return identifyData(root, bytes, 'eventNode');
}

export function identifyInstructionData(
root: RootNode,
bytes: ReadonlyUint8Array | Uint8Array,
): NodePath<InstructionNode> | undefined {
return identifyData(root, bytes, 'instructionNode');
}

export function identifyData<TKind extends 'accountNode' | 'instructionNode'>(
export function identifyData<TKind extends 'accountNode' | 'eventNode' | 'instructionNode'>(
root: RootNode,
bytes: ReadonlyUint8Array | Uint8Array,
kind?: TKind | TKind[],
Expand All @@ -46,7 +54,7 @@ export function identifyData<TKind extends 'accountNode' | 'instructionNode'>(

const codecAndValueVisitors = getCodecAndValueVisitors(linkables, { stack });
const visitor = getByteIdentificationVisitor(
kind ?? (['accountNode', 'instructionNode'] as TKind[]),
kind ?? (['accountNode', 'instructionNode', 'eventNode'] as TKind[]),
bytes,
codecAndValueVisitors,
{ stack },
Expand All @@ -55,7 +63,7 @@ export function identifyData<TKind extends 'accountNode' | 'instructionNode'>(
return visit(root, visitor);
}

export function getByteIdentificationVisitor<TKind extends 'accountNode' | 'instructionNode'>(
export function getByteIdentificationVisitor<TKind extends 'accountNode' | 'eventNode' | 'instructionNode'>(
kind: TKind | TKind[],
bytes: ReadonlyUint8Array | Uint8Array,
codecAndValueVisitors: CodecAndValueVisitors,
Expand All @@ -71,14 +79,24 @@ export function getByteIdentificationVisitor<TKind extends 'accountNode' | 'inst
const match = matchDiscriminators(bytes, node.discriminators, struct, codecAndValueVisitors);
return match ? stack.getPath(node.kind) : undefined;
},
visitEvent(node) {
if (!node.discriminators) return;
const match = matchDiscriminators(
bytes,
node.discriminators,
resolveNestedTypeNode(node.data),
codecAndValueVisitors,
);
return match ? stack.getPath(node.kind) : undefined;
},
visitInstruction(node) {
if (!node.discriminators) return;
const struct = structTypeNodeFromInstructionArgumentNodes(node.arguments);
const match = matchDiscriminators(bytes, node.discriminators, struct, codecAndValueVisitors);
return match ? stack.getPath(node.kind) : undefined;
},
visitProgram(node) {
const candidates = [...node.accounts, ...node.instructions].filter(isNodeFilter(kind));
const candidates = [...node.accounts, ...node.events, ...node.instructions].filter(isNodeFilter(kind));
for (const candidate of candidates) {
const result = visit(candidate, this);
if (result) return result;
Expand All @@ -89,7 +107,7 @@ export function getByteIdentificationVisitor<TKind extends 'accountNode' | 'inst
},
} as Visitor<
NodePath<GetNodeFromKind<TKind>> | undefined,
'accountNode' | 'instructionNode' | 'programNode' | 'rootNode'
'accountNode' | 'eventNode' | 'instructionNode' | 'programNode' | 'rootNode'
>,
v => recordNodeStackVisitor(v, stack),
);
Expand Down
20 changes: 15 additions & 5 deletions packages/dynamic-parsers/src/parsers.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { getNodeCodec, ReadonlyUint8Array } from '@codama/dynamic-codecs';
import { AccountNode, CamelCaseString, GetNodeFromKind, InstructionNode, RootNode } from '@codama/nodes';
import { AccountNode, CamelCaseString, EventNode, GetNodeFromKind, InstructionNode, RootNode } from '@codama/nodes';
import { getLastNodeFromPath, NodePath } from '@codama/visitors-core';
import type {
AccountLookupMeta,
Expand All @@ -11,7 +11,10 @@ import type {

import { identifyData } from './identify';

export type ParsedData<TNode extends AccountNode | InstructionNode> = {
type ParsableNode = AccountNode | EventNode | InstructionNode;
type ParsableNodeKind = ParsableNode['kind'];

export type ParsedData<TNode extends ParsableNode> = {
data: unknown;
path: NodePath<TNode>;
};
Expand All @@ -23,21 +26,28 @@ export function parseAccountData(
return parseData(root, bytes, 'accountNode');
}

export function parseEventData(
root: RootNode,
bytes: ReadonlyUint8Array | Uint8Array,
): ParsedData<EventNode> | undefined {
return parseData(root, bytes, 'eventNode');
}

export function parseInstructionData(
root: RootNode,
bytes: ReadonlyUint8Array | Uint8Array,
): ParsedData<InstructionNode> | undefined {
return parseData(root, bytes, 'instructionNode');
}

export function parseData<TKind extends 'accountNode' | 'instructionNode'>(
export function parseData<TKind extends ParsableNodeKind>(
root: RootNode,
bytes: ReadonlyUint8Array | Uint8Array,
kind?: TKind | TKind[],
): ParsedData<GetNodeFromKind<TKind>> | undefined {
const path = identifyData<TKind>(root, bytes, kind ?? (['accountNode', 'instructionNode'] as TKind[]));
const path = identifyData<TKind>(root, bytes, kind ?? (['accountNode', 'instructionNode', 'eventNode'] as TKind[]));
if (!path) return undefined;
const codec = getNodeCodec(path as NodePath<AccountNode | InstructionNode>);
const codec = getNodeCodec(path as NodePath<ParsableNode>);
const data = codec.decode(bytes);
return { data, path };
}
Expand Down
Loading
Loading