Skip to content

Commit 446de11

Browse files
committed
Add dynamic-parsers package
1 parent 4bcf5b6 commit 446de11

File tree

16 files changed

+442
-0
lines changed

16 files changed

+442
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,23 @@
1+
import { LinkableDictionary, NodeStack } from '@codama/visitors-core';
2+
import { containsBytes, ReadonlyUint8Array } from '@solana/codecs';
3+
4+
import { getNodeCodecVisitor } from './codecs';
5+
import { getValueNodeVisitor } from './values';
6+
17
export * from './codecs';
28
export * from './values';
9+
10+
export type { ReadonlyUint8Array };
11+
export { containsBytes };
12+
13+
export type CodecAndValueVisitors = {
14+
codecVisitor: ReturnType<typeof getNodeCodecVisitor>;
15+
valueVisitor: ReturnType<typeof getValueNodeVisitor>;
16+
};
17+
18+
export function getCodecAndValueVisitors(linkables: LinkableDictionary, options: { stack?: NodeStack } = {}) {
19+
const stack = options.stack ?? new NodeStack();
20+
const codecVisitor = getNodeCodecVisitor(linkables, { stack });
21+
const valueVisitor = getValueNodeVisitor(linkables, { codecVisitorFactory: () => codecVisitor, stack });
22+
return { codecVisitor, valueVisitor };
23+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
dist/
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
dist/
2+
e2e/
3+
test-ledger/
4+
target/
5+
CHANGELOG.md

packages/dynamic-parsers/LICENSE

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
MIT License
2+
3+
Copyright (c) 2024 Codama
4+
5+
Permission is hereby granted, free of charge, to any person obtaining
6+
a copy of this software and associated documentation files (the
7+
"Software"), to deal in the Software without restriction, including
8+
without limitation the rights to use, copy, modify, merge, publish,
9+
distribute, sublicense, and/or sell copies of the Software, and to
10+
permit persons to whom the Software is furnished to do so, subject to
11+
the following conditions:
12+
13+
The above copyright notice and this permission notice shall be
14+
included in all copies or substantial portions of the Software.
15+
16+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

packages/dynamic-parsers/README.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Codama ➤ Dynamic Parsers
2+
3+
[![npm][npm-image]][npm-url]
4+
[![npm-downloads][npm-downloads-image]][npm-url]
5+
6+
[npm-downloads-image]: https://img.shields.io/npm/dm/@codama/dynamic-parsers.svg?style=flat
7+
[npm-image]: https://img.shields.io/npm/v/@codama/dynamic-parsers.svg?style=flat&label=%40codama%2Fdynamic-parsers
8+
[npm-url]: https://www.npmjs.com/package/@codama/dynamic-parsers
9+
10+
This package provides a set of helpers that, given any Codama IDL, dynamically identifies and parses any byte array into deserialized accounts, instructions and defined types.
11+
12+
## Installation
13+
14+
```sh
15+
pnpm install @codama/dynamic-parsers
16+
```
17+
18+
> [!NOTE]
19+
> This package is **not** included in the main [`codama`](../library) package.
20+
21+
## Functions
22+
23+
TODO
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
{
2+
"name": "@codama/dynamic-parsers",
3+
"version": "1.0.0",
4+
"description": "Helpers to dynamically identify and parse accounts and instructions",
5+
"exports": {
6+
"types": "./dist/types/index.d.ts",
7+
"react-native": "./dist/index.react-native.mjs",
8+
"browser": {
9+
"import": "./dist/index.browser.mjs",
10+
"require": "./dist/index.browser.cjs"
11+
},
12+
"node": {
13+
"import": "./dist/index.node.mjs",
14+
"require": "./dist/index.node.cjs"
15+
}
16+
},
17+
"browser": {
18+
"./dist/index.node.cjs": "./dist/index.browser.cjs",
19+
"./dist/index.node.mjs": "./dist/index.browser.mjs"
20+
},
21+
"main": "./dist/index.node.cjs",
22+
"module": "./dist/index.node.mjs",
23+
"react-native": "./dist/index.react-native.mjs",
24+
"types": "./dist/types/index.d.ts",
25+
"type": "commonjs",
26+
"files": [
27+
"./dist/types",
28+
"./dist/index.*"
29+
],
30+
"sideEffects": false,
31+
"keywords": [
32+
"solana",
33+
"framework",
34+
"standard",
35+
"specifications",
36+
"parsers"
37+
],
38+
"scripts": {
39+
"build": "rimraf dist && pnpm build:src && pnpm build:types",
40+
"build:src": "zx ../../node_modules/@codama/internals/scripts/build-src.mjs package",
41+
"build:types": "zx ../../node_modules/@codama/internals/scripts/build-types.mjs",
42+
"dev": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs node --watch",
43+
"lint": "zx ../../node_modules/@codama/internals/scripts/lint.mjs",
44+
"lint:fix": "zx ../../node_modules/@codama/internals/scripts/lint.mjs --fix",
45+
"test": "pnpm test:types && pnpm test:treeshakability && pnpm test:browser && pnpm test:node && pnpm test:react-native",
46+
"test:browser": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs browser",
47+
"test:node": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs node",
48+
"test:react-native": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs react-native",
49+
"test:treeshakability": "zx ../../node_modules/@codama/internals/scripts/test-treeshakability.mjs",
50+
"test:types": "zx ../../node_modules/@codama/internals/scripts/test-types.mjs"
51+
},
52+
"dependencies": {
53+
"@codama/dynamic-codecs": "workspace:*",
54+
"@codama/errors": "workspace:*",
55+
"@codama/nodes": "workspace:*",
56+
"@codama/visitors-core": "workspace:*",
57+
"@solana/instructions": "2.0.0-rc.4"
58+
},
59+
"license": "MIT",
60+
"repository": {
61+
"type": "git",
62+
"url": "https://github.com/codama-idl/codama"
63+
},
64+
"bugs": {
65+
"url": "http://github.com/codama-idl/codama/issues"
66+
},
67+
"browserslist": [
68+
"supports bigint and not dead",
69+
"maintained node versions"
70+
]
71+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { CodecAndValueVisitors, containsBytes, ReadonlyUint8Array } from '@codama/dynamic-codecs';
2+
import {
3+
assertIsNode,
4+
ConstantDiscriminatorNode,
5+
constantDiscriminatorNode,
6+
constantValueNode,
7+
DiscriminatorNode,
8+
FieldDiscriminatorNode,
9+
isNode,
10+
SizeDiscriminatorNode,
11+
StructTypeNode,
12+
} from '@codama/nodes';
13+
import { visit } from '@codama/visitors-core';
14+
15+
export function matchDiscriminators(
16+
bytes: ReadonlyUint8Array,
17+
discriminators: DiscriminatorNode[],
18+
struct: StructTypeNode,
19+
visitors: CodecAndValueVisitors,
20+
): boolean {
21+
return discriminators.every(discriminator => matchDiscriminator(bytes, discriminator, struct, visitors));
22+
}
23+
24+
function matchDiscriminator(
25+
bytes: ReadonlyUint8Array,
26+
discriminator: DiscriminatorNode,
27+
struct: StructTypeNode,
28+
visitors: CodecAndValueVisitors,
29+
): boolean {
30+
if (isNode(discriminator, 'constantDiscriminatorNode')) {
31+
return matchConstantDiscriminator(bytes, discriminator, visitors);
32+
}
33+
if (isNode(discriminator, 'fieldDiscriminatorNode')) {
34+
return matchFieldDiscriminator(bytes, discriminator, struct, visitors);
35+
}
36+
assertIsNode(discriminator, 'sizeDiscriminatorNode');
37+
return matchSizeDiscriminator(bytes, discriminator);
38+
}
39+
40+
function matchConstantDiscriminator(
41+
bytes: ReadonlyUint8Array,
42+
discriminator: ConstantDiscriminatorNode,
43+
{ codecVisitor, valueVisitor }: CodecAndValueVisitors,
44+
): boolean {
45+
const codec = visit(discriminator.constant.type, codecVisitor);
46+
const value = visit(discriminator.constant.value, valueVisitor);
47+
const bytesToMatch = codec.encode(value);
48+
return containsBytes(bytes, bytesToMatch, discriminator.offset);
49+
}
50+
51+
function matchFieldDiscriminator(
52+
bytes: ReadonlyUint8Array,
53+
discriminator: FieldDiscriminatorNode,
54+
struct: StructTypeNode,
55+
visitors: CodecAndValueVisitors,
56+
): boolean {
57+
const field = struct.fields.find(field => field.name === discriminator.name);
58+
if (!field) {
59+
// TODO: Coded error.
60+
throw new Error('Discriminator field not found');
61+
}
62+
if (!field.defaultValue) {
63+
// TODO: Coded error.
64+
throw new Error('Discriminator field must have a default value');
65+
}
66+
const constantNode = constantValueNode(field.type, field.defaultValue);
67+
const constantDiscriminator = constantDiscriminatorNode(constantNode, discriminator.offset);
68+
return matchConstantDiscriminator(bytes, constantDiscriminator, visitors);
69+
}
70+
71+
function matchSizeDiscriminator(bytes: ReadonlyUint8Array, discriminator: SizeDiscriminatorNode): boolean {
72+
return bytes.length === discriminator.size;
73+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { CodecAndValueVisitors, getCodecAndValueVisitors, ReadonlyUint8Array } from '@codama/dynamic-codecs';
2+
import {
3+
AccountNode,
4+
GetNodeFromKind,
5+
InstructionNode,
6+
isNodeFilter,
7+
resolveNestedTypeNode,
8+
RootNode,
9+
structTypeNodeFromInstructionArgumentNodes,
10+
} from '@codama/nodes';
11+
import {
12+
getRecordLinkablesVisitor,
13+
LinkableDictionary,
14+
NodePath,
15+
NodeStack,
16+
pipe,
17+
recordNodeStackVisitor,
18+
visit,
19+
Visitor,
20+
} from '@codama/visitors-core';
21+
22+
import { matchDiscriminators } from './discriminators';
23+
24+
export function identifyAccountData(
25+
root: RootNode,
26+
bytes: ReadonlyUint8Array | Uint8Array,
27+
): NodePath<AccountNode> | undefined {
28+
return identifyData(root, bytes, 'accountNode');
29+
}
30+
31+
export function identifyInstructionData(
32+
root: RootNode,
33+
bytes: ReadonlyUint8Array | Uint8Array,
34+
): NodePath<InstructionNode> | undefined {
35+
return identifyData(root, bytes, 'instructionNode');
36+
}
37+
38+
export function identifyData<TKind extends 'accountNode' | 'instructionNode'>(
39+
root: RootNode,
40+
bytes: ReadonlyUint8Array | Uint8Array,
41+
kind?: TKind | TKind[],
42+
): NodePath<GetNodeFromKind<TKind>> | undefined {
43+
const stack = new NodeStack();
44+
const linkables = new LinkableDictionary();
45+
visit(root, getRecordLinkablesVisitor(linkables));
46+
47+
const codecAndValueVisitors = getCodecAndValueVisitors(linkables, { stack });
48+
const visitor = getByteIdentificationVisitor(
49+
kind ?? (['accountNode', 'instructionNode'] as TKind[]),
50+
bytes,
51+
codecAndValueVisitors,
52+
{ stack },
53+
);
54+
55+
return visit(root, visitor);
56+
}
57+
58+
export function getByteIdentificationVisitor<TKind extends 'accountNode' | 'instructionNode'>(
59+
kind: TKind | TKind[],
60+
bytes: ReadonlyUint8Array | Uint8Array,
61+
codecAndValueVisitors: CodecAndValueVisitors,
62+
options: { stack?: NodeStack } = {},
63+
) {
64+
const stack = options.stack ?? new NodeStack();
65+
66+
return pipe(
67+
{
68+
visitAccount(node) {
69+
if (!node.discriminators) return;
70+
const struct = resolveNestedTypeNode(node.data);
71+
const match = matchDiscriminators(bytes, node.discriminators, struct, codecAndValueVisitors);
72+
return match ? stack.getPath(node.kind) : undefined;
73+
},
74+
visitInstruction(node) {
75+
if (!node.discriminators) return;
76+
const struct = structTypeNodeFromInstructionArgumentNodes(node.arguments);
77+
const match = matchDiscriminators(bytes, node.discriminators, struct, codecAndValueVisitors);
78+
return match ? stack.getPath(node.kind) : undefined;
79+
},
80+
visitProgram(node) {
81+
const candidates = [...node.accounts, ...node.instructions].filter(isNodeFilter(kind));
82+
for (const candidate of candidates) {
83+
const result = visit(candidate, this);
84+
if (result) return result;
85+
}
86+
},
87+
visitRoot(node) {
88+
return visit(node.program, this);
89+
},
90+
} as Visitor<
91+
NodePath<GetNodeFromKind<TKind>> | undefined,
92+
'accountNode' | 'instructionNode' | 'programNode' | 'rootNode'
93+
>,
94+
v => recordNodeStackVisitor(v, stack),
95+
);
96+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
export * from './discriminators';
2+
export * from './identify';
3+
export * from './parsers';
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import { getNodeCodec, ReadonlyUint8Array } from '@codama/dynamic-codecs';
2+
import { AccountNode, CamelCaseString, GetNodeFromKind, InstructionNode, RootNode } from '@codama/nodes';
3+
import { getLastNodeFromPath, NodePath } from '@codama/visitors-core';
4+
import type {
5+
IAccountLookupMeta,
6+
IAccountMeta,
7+
IInstruction,
8+
IInstructionWithAccounts,
9+
IInstructionWithData,
10+
} from '@solana/instructions';
11+
12+
import { identifyData } from './identify';
13+
14+
export type ParsedData<TNode extends AccountNode | InstructionNode> = {
15+
data: unknown;
16+
path: NodePath<TNode>;
17+
};
18+
19+
export function parseAccountData(
20+
root: RootNode,
21+
bytes: ReadonlyUint8Array | Uint8Array,
22+
): ParsedData<AccountNode> | undefined {
23+
return parseData(root, bytes, 'accountNode');
24+
}
25+
26+
export function parseInstructionData(
27+
root: RootNode,
28+
bytes: ReadonlyUint8Array | Uint8Array,
29+
): ParsedData<InstructionNode> | undefined {
30+
return parseData(root, bytes, 'instructionNode');
31+
}
32+
33+
export function parseData<TKind extends 'accountNode' | 'instructionNode'>(
34+
root: RootNode,
35+
bytes: ReadonlyUint8Array | Uint8Array,
36+
kind?: TKind | TKind[],
37+
): ParsedData<GetNodeFromKind<TKind>> | undefined {
38+
const path = identifyData<TKind>(root, bytes, kind ?? (['accountNode', 'instructionNode'] as TKind[]));
39+
if (!path) return undefined;
40+
const codec = getNodeCodec(path as NodePath<AccountNode | InstructionNode>);
41+
const data = codec.decode(bytes);
42+
return { data, path };
43+
}
44+
45+
type ParsedInstructionAccounts = ReadonlyArray<IAccountMeta & { name: CamelCaseString }>;
46+
type ParsedInstruction = ParsedData<InstructionNode> & { accounts: ParsedInstructionAccounts };
47+
48+
export function parseInstruction(
49+
root: RootNode,
50+
instruction: IInstruction &
51+
IInstructionWithAccounts<readonly (IAccountLookupMeta | IAccountMeta)[]> &
52+
IInstructionWithData<Uint8Array>,
53+
): ParsedInstruction | undefined {
54+
const parsedData = parseInstructionData(root, instruction.data);
55+
if (!parsedData) return undefined;
56+
instruction.accounts;
57+
const instructionNode = getLastNodeFromPath(parsedData.path);
58+
const accounts: ParsedInstructionAccounts = instructionNode.accounts.flatMap((account, index) => {
59+
const accountMeta = instruction.accounts[index];
60+
if (!accountMeta) return [];
61+
return [{ ...accountMeta, name: account.name }];
62+
});
63+
return { ...parsedData, accounts };
64+
}

0 commit comments

Comments
 (0)