Skip to content

Commit 35516d4

Browse files
authored
feat: add event support to dynamic parsers and codecs (#989)
1 parent 6487af5 commit 35516d4

9 files changed

Lines changed: 286 additions & 19 deletions

File tree

.changeset/four-spoons-fix.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
"@codama/dynamic-codecs": minor
3+
"@codama/dynamic-parsers": minor
4+
---
5+
6+
Add event support to dynamic codecs and parsers.

packages/dynamic-codecs/src/codecs.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import {
1010
CountNode,
1111
DefinedTypeLinkNode,
1212
DefinedTypeNode,
13+
EventNode,
1314
InstructionArgumentLinkNode,
1415
InstructionArgumentNode,
1516
InstructionLinkNode,
@@ -85,6 +86,7 @@ export type EncodableNodes =
8586
| AccountNode
8687
| DefinedTypeLinkNode
8788
| DefinedTypeNode
89+
| EventNode
8890
| InstructionArgumentLinkNode
8991
| InstructionArgumentNode
9092
| InstructionLinkNode
@@ -199,6 +201,9 @@ export function getNodeCodecVisitor(
199201
const variants = node.variants.map(variant => [pascalCase(variant.name), visit(variant, this)] as const);
200202
return getDiscriminatedUnionCodec(variants, { size }) as unknown as Codec<unknown>;
201203
},
204+
visitEvent(node) {
205+
return visit(node.data, this);
206+
},
202207
visitFixedSizeType(node) {
203208
const type = visit(node.type, this);
204209
return fixCodecSize(type, node.size);
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { eventNode, numberTypeNode, structFieldTypeNode, structTypeNode } from '@codama/nodes';
2+
import { expect, test } from 'vitest';
3+
4+
import { getNodeCodec } from '../../src';
5+
import { hex } from '../_setup';
6+
7+
test('it delegates to the underlying event data node', () => {
8+
const codec = getNodeCodec([
9+
eventNode({
10+
data: structTypeNode([
11+
structFieldTypeNode({
12+
name: 'foo',
13+
type: numberTypeNode('u32'),
14+
}),
15+
]),
16+
name: 'myEvent',
17+
}),
18+
]);
19+
expect(codec.encode({ foo: 42 })).toStrictEqual(hex('2a000000'));
20+
expect(codec.decode(hex('2a000000'))).toStrictEqual({ foo: 42 });
21+
});

packages/dynamic-parsers/README.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
[npm-image]: https://img.shields.io/npm/v/@codama/dynamic-parsers.svg?style=flat&label=%40codama%2Fdynamic-parsers
88
[npm-url]: https://www.npmjs.com/package/@codama/dynamic-parsers
99

10-
This package provides a set of helpers that, given any Codama IDL, dynamically identifies and parses any byte array into deserialized accounts and instructions.
10+
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.
1111

1212
## Installation
1313

@@ -25,7 +25,7 @@ pnpm install @codama/dynamic-parsers
2525
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.
2626

2727
```ts
28-
type ParsedData<TNode extends AccountNode | InstructionNode> = {
28+
type ParsedData<TNode extends AccountNode | EventNode | InstructionNode> = {
2929
data: unknown;
3030
path: NodePath<TNode>;
3131
};
@@ -47,6 +47,20 @@ if (parsedData) {
4747
}
4848
```
4949

50+
### `parseEventData(rootNode, bytes)`
51+
52+
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.
53+
54+
```ts
55+
const parsedData = parseEventData(rootNode, bytes);
56+
// ^ ParsedData<EventNode> | undefined
57+
58+
if (parsedData) {
59+
const eventNode: EventNode = getLastNodeFromPath(parsedData.path);
60+
const decodedData: unknown = parsedData.data;
61+
}
62+
```
63+
5064
### `parseInstructionData(rootNode, bytes)`
5165

5266
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.
@@ -99,3 +113,16 @@ if (path) {
99113
const instructionNode: InstructionNode = getLastNodeFromPath(path);
100114
}
101115
```
116+
117+
### `identifyEventData`
118+
119+
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.
120+
121+
```ts
122+
const path = identifyEventData(root, bytes);
123+
// ^ NodePath<EventNode> | undefined
124+
125+
if (path) {
126+
const eventNode: EventNode = getLastNodeFromPath(path);
127+
}
128+
```

packages/dynamic-parsers/src/discriminators.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,32 +14,33 @@ import {
1414
isNode,
1515
SizeDiscriminatorNode,
1616
StructTypeNode,
17+
TypeNode,
1718
} from '@codama/nodes';
1819
import { visit } from '@codama/visitors-core';
1920

2021
export function matchDiscriminators(
2122
bytes: ReadonlyUint8Array,
2223
discriminators: DiscriminatorNode[],
23-
struct: StructTypeNode,
24+
typeNode: TypeNode,
2425
visitors: CodecAndValueVisitors,
2526
): boolean {
2627
return (
2728
discriminators.length > 0 &&
28-
discriminators.every(discriminator => matchDiscriminator(bytes, discriminator, struct, visitors))
29+
discriminators.every(discriminator => matchDiscriminator(bytes, discriminator, typeNode, visitors))
2930
);
3031
}
3132

3233
function matchDiscriminator(
3334
bytes: ReadonlyUint8Array,
3435
discriminator: DiscriminatorNode,
35-
struct: StructTypeNode,
36+
typeNode: TypeNode,
3637
visitors: CodecAndValueVisitors,
3738
): boolean {
3839
if (isNode(discriminator, 'constantDiscriminatorNode')) {
3940
return matchConstantDiscriminator(bytes, discriminator, visitors);
4041
}
4142
if (isNode(discriminator, 'fieldDiscriminatorNode')) {
42-
return matchFieldDiscriminator(bytes, discriminator, struct, visitors);
43+
return matchFieldDiscriminator(bytes, discriminator, typeNode, visitors);
4344
}
4445
assertIsNode(discriminator, 'sizeDiscriminatorNode');
4546
return matchSizeDiscriminator(bytes, discriminator);
@@ -59,9 +60,15 @@ function matchConstantDiscriminator(
5960
function matchFieldDiscriminator(
6061
bytes: ReadonlyUint8Array,
6162
discriminator: FieldDiscriminatorNode,
62-
struct: StructTypeNode,
63+
typeNode: TypeNode,
6364
visitors: CodecAndValueVisitors,
6465
): boolean {
66+
if (!isNode(typeNode, 'structTypeNode')) {
67+
throw new CodamaError(CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND, {
68+
field: discriminator.name,
69+
});
70+
}
71+
const struct = typeNode as StructTypeNode;
6572
const field = struct.fields.find(field => field.name === discriminator.name);
6673
if (!field) {
6774
throw new CodamaError(CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND, {

packages/dynamic-parsers/src/identify.ts

Lines changed: 23 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CodecAndValueVisitors, getCodecAndValueVisitors, ReadonlyUint8Array } from '@codama/dynamic-codecs';
22
import {
33
AccountNode,
4+
EventNode,
45
GetNodeFromKind,
56
InstructionNode,
67
isNodeFilter,
@@ -28,14 +29,21 @@ export function identifyAccountData(
2829
return identifyData(root, bytes, 'accountNode');
2930
}
3031

32+
export function identifyEventData(
33+
root: RootNode,
34+
bytes: ReadonlyUint8Array | Uint8Array,
35+
): NodePath<EventNode> | undefined {
36+
return identifyData(root, bytes, 'eventNode');
37+
}
38+
3139
export function identifyInstructionData(
3240
root: RootNode,
3341
bytes: ReadonlyUint8Array | Uint8Array,
3442
): NodePath<InstructionNode> | undefined {
3543
return identifyData(root, bytes, 'instructionNode');
3644
}
3745

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

4755
const codecAndValueVisitors = getCodecAndValueVisitors(linkables, { stack });
4856
const visitor = getByteIdentificationVisitor(
49-
kind ?? (['accountNode', 'instructionNode'] as TKind[]),
57+
kind ?? (['accountNode', 'instructionNode', 'eventNode'] as TKind[]),
5058
bytes,
5159
codecAndValueVisitors,
5260
{ stack },
@@ -55,7 +63,7 @@ export function identifyData<TKind extends 'accountNode' | 'instructionNode'>(
5563
return visit(root, visitor);
5664
}
5765

58-
export function getByteIdentificationVisitor<TKind extends 'accountNode' | 'instructionNode'>(
66+
export function getByteIdentificationVisitor<TKind extends 'accountNode' | 'eventNode' | 'instructionNode'>(
5967
kind: TKind | TKind[],
6068
bytes: ReadonlyUint8Array | Uint8Array,
6169
codecAndValueVisitors: CodecAndValueVisitors,
@@ -71,14 +79,24 @@ export function getByteIdentificationVisitor<TKind extends 'accountNode' | 'inst
7179
const match = matchDiscriminators(bytes, node.discriminators, struct, codecAndValueVisitors);
7280
return match ? stack.getPath(node.kind) : undefined;
7381
},
82+
visitEvent(node) {
83+
if (!node.discriminators) return;
84+
const match = matchDiscriminators(
85+
bytes,
86+
node.discriminators,
87+
resolveNestedTypeNode(node.data),
88+
codecAndValueVisitors,
89+
);
90+
return match ? stack.getPath(node.kind) : undefined;
91+
},
7492
visitInstruction(node) {
7593
if (!node.discriminators) return;
7694
const struct = structTypeNodeFromInstructionArgumentNodes(node.arguments);
7795
const match = matchDiscriminators(bytes, node.discriminators, struct, codecAndValueVisitors);
7896
return match ? stack.getPath(node.kind) : undefined;
7997
},
8098
visitProgram(node) {
81-
const candidates = [...node.accounts, ...node.instructions].filter(isNodeFilter(kind));
99+
const candidates = [...node.accounts, ...node.events, ...node.instructions].filter(isNodeFilter(kind));
82100
for (const candidate of candidates) {
83101
const result = visit(candidate, this);
84102
if (result) return result;
@@ -89,7 +107,7 @@ export function getByteIdentificationVisitor<TKind extends 'accountNode' | 'inst
89107
},
90108
} as Visitor<
91109
NodePath<GetNodeFromKind<TKind>> | undefined,
92-
'accountNode' | 'instructionNode' | 'programNode' | 'rootNode'
110+
'accountNode' | 'eventNode' | 'instructionNode' | 'programNode' | 'rootNode'
93111
>,
94112
v => recordNodeStackVisitor(v, stack),
95113
);

packages/dynamic-parsers/src/parsers.ts

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { getNodeCodec, ReadonlyUint8Array } from '@codama/dynamic-codecs';
2-
import { AccountNode, CamelCaseString, GetNodeFromKind, InstructionNode, RootNode } from '@codama/nodes';
2+
import { AccountNode, CamelCaseString, EventNode, GetNodeFromKind, InstructionNode, RootNode } from '@codama/nodes';
33
import { getLastNodeFromPath, NodePath } from '@codama/visitors-core';
44
import type {
55
AccountLookupMeta,
@@ -11,7 +11,10 @@ import type {
1111

1212
import { identifyData } from './identify';
1313

14-
export type ParsedData<TNode extends AccountNode | InstructionNode> = {
14+
type ParsableNode = AccountNode | EventNode | InstructionNode;
15+
type ParsableNodeKind = ParsableNode['kind'];
16+
17+
export type ParsedData<TNode extends ParsableNode> = {
1518
data: unknown;
1619
path: NodePath<TNode>;
1720
};
@@ -23,21 +26,28 @@ export function parseAccountData(
2326
return parseData(root, bytes, 'accountNode');
2427
}
2528

29+
export function parseEventData(
30+
root: RootNode,
31+
bytes: ReadonlyUint8Array | Uint8Array,
32+
): ParsedData<EventNode> | undefined {
33+
return parseData(root, bytes, 'eventNode');
34+
}
35+
2636
export function parseInstructionData(
2737
root: RootNode,
2838
bytes: ReadonlyUint8Array | Uint8Array,
2939
): ParsedData<InstructionNode> | undefined {
3040
return parseData(root, bytes, 'instructionNode');
3141
}
3242

33-
export function parseData<TKind extends 'accountNode' | 'instructionNode'>(
43+
export function parseData<TKind extends ParsableNodeKind>(
3444
root: RootNode,
3545
bytes: ReadonlyUint8Array | Uint8Array,
3646
kind?: TKind | TKind[],
3747
): ParsedData<GetNodeFromKind<TKind>> | undefined {
38-
const path = identifyData<TKind>(root, bytes, kind ?? (['accountNode', 'instructionNode'] as TKind[]));
48+
const path = identifyData<TKind>(root, bytes, kind ?? (['accountNode', 'instructionNode', 'eventNode'] as TKind[]));
3949
if (!path) return undefined;
40-
const codec = getNodeCodec(path as NodePath<AccountNode | InstructionNode>);
50+
const codec = getNodeCodec(path as NodePath<ParsableNode>);
4151
const data = codec.decode(bytes);
4252
return { data, path };
4353
}

0 commit comments

Comments
 (0)