瀏覽代碼

Add `dynamic-parsers` package (#294)

Loris Leiva 1 年之前
父節點
當前提交
c698fe5555

+ 5 - 0
.changeset/wicked-radios-cough.md

@@ -0,0 +1,5 @@
+---
+'@codama/dynamic-parsers': minor
+---
+
+Add new `dynamic-parsers` package that identifies accounts and instructions from a root node and decodes the provided byte array accordingly

+ 21 - 0
packages/dynamic-codecs/src/index.ts

@@ -1,2 +1,23 @@
+import { LinkableDictionary, NodeStack } from '@codama/visitors-core';
+import { containsBytes, ReadonlyUint8Array } from '@solana/codecs';
+
+import { getNodeCodecVisitor } from './codecs';
+import { getValueNodeVisitor } from './values';
+
 export * from './codecs';
 export * from './values';
+
+export type { ReadonlyUint8Array };
+export { containsBytes };
+
+export type CodecAndValueVisitors = {
+    codecVisitor: ReturnType<typeof getNodeCodecVisitor>;
+    valueVisitor: ReturnType<typeof getValueNodeVisitor>;
+};
+
+export function getCodecAndValueVisitors(linkables: LinkableDictionary, options: { stack?: NodeStack } = {}) {
+    const stack = options.stack ?? new NodeStack();
+    const codecVisitor = getNodeCodecVisitor(linkables, { stack });
+    const valueVisitor = getValueNodeVisitor(linkables, { codecVisitorFactory: () => codecVisitor, stack });
+    return { codecVisitor, valueVisitor };
+}

+ 1 - 0
packages/dynamic-parsers/.gitignore

@@ -0,0 +1 @@
+dist/

+ 5 - 0
packages/dynamic-parsers/.prettierignore

@@ -0,0 +1,5 @@
+dist/
+e2e/
+test-ledger/
+target/
+CHANGELOG.md

+ 22 - 0
packages/dynamic-parsers/LICENSE

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

+ 101 - 0
packages/dynamic-parsers/README.md

@@ -0,0 +1,101 @@
+# Codama ➤ Dynamic Parsers
+
+[![npm][npm-image]][npm-url]
+[![npm-downloads][npm-downloads-image]][npm-url]
+
+[npm-downloads-image]: https://img.shields.io/npm/dm/@codama/dynamic-parsers.svg?style=flat
+[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.
+
+## Installation
+
+```sh
+pnpm install @codama/dynamic-parsers
+```
+
+> [!NOTE]
+> This package is **not** included in the main [`codama`](../library) package.
+
+## Types
+
+### `ParsedData<TNode>`
+
+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> = {
+    data: unknown;
+    path: NodePath<TNode>;
+};
+```
+
+## Functions
+
+### `parseAccountData(rootNode, bytes)`
+
+Given a `RootNode` and a byte array, this function will attempt to identify the correct account node and use it to deserialize the provided bytes. Therefore, it returns a `ParsedData<AccountNode>` object if the parsing was successful, or `undefined` otherwise.
+
+```ts
+const parsedData = parseAccountData(rootNode, bytes);
+// ^ ParsedData<AccountNode> | undefined
+
+if (parsedData) {
+    const accountNode: AccountNode = 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.
+
+```ts
+const parsedData = parseInstructionData(rootNode, bytes);
+// ^ ParsedData<InstructionNode> | undefined
+
+if (parsedData) {
+    const instructionNode: InstructionNode = getLastNodeFromPath(parsedData.path);
+    const decodedData: unknown = parsedData.data;
+}
+```
+
+### `parseInstruction(rootNode, instruction)`
+
+This function accepts a `RootNode` and an `IInstruction` type — as defined in `@solana/instructions` — in order to return a `ParsedData<InstructionNode>` object that also includes an `accounts` array that match each `IAccountMeta` with its corresponding account name.
+
+```ts
+const parsedData = parseInstruction(rootNode, instruction);
+
+if (parsedData) {
+    const namedAccounts = parsedData.accounts;
+    // ^ Array<IAccountMeta & { name: string }>
+}
+```
+
+### `identifyAccountData`
+
+This function tries to match the provided bytes to an account node, returning a `NodePath<AccountNode>` object if the identification was successful, or `undefined` otherwise. It is used by the `parseAccountData` function under the hood.
+
+```ts
+const path = identifyAccountData(root, bytes);
+// ^ NodePath<AccountNode> | undefined
+
+if (path) {
+    const accountNode: AccountNode = getLastNodeFromPath(path);
+}
+```
+
+### `identifyInstructionData`
+
+This function tries to match the provided bytes to an instruction node, returning a `NodePath<InstructionNode>` object if the identification was successful, or `undefined` otherwise. It is used by the `parseInstructionData` function under the hood.
+
+```ts
+const path = identifyInstructionData(root, bytes);
+// ^ NodePath<InstructionNode> | undefined
+
+if (path) {
+    const instructionNode: InstructionNode = getLastNodeFromPath(path);
+}
+```

+ 74 - 0
packages/dynamic-parsers/package.json

@@ -0,0 +1,74 @@
+{
+    "name": "@codama/dynamic-parsers",
+    "version": "1.0.0",
+    "description": "Helpers to dynamically identify and parse accounts and instructions",
+    "exports": {
+        "types": "./dist/types/index.d.ts",
+        "react-native": "./dist/index.react-native.mjs",
+        "browser": {
+            "import": "./dist/index.browser.mjs",
+            "require": "./dist/index.browser.cjs"
+        },
+        "node": {
+            "import": "./dist/index.node.mjs",
+            "require": "./dist/index.node.cjs"
+        }
+    },
+    "browser": {
+        "./dist/index.node.cjs": "./dist/index.browser.cjs",
+        "./dist/index.node.mjs": "./dist/index.browser.mjs"
+    },
+    "main": "./dist/index.node.cjs",
+    "module": "./dist/index.node.mjs",
+    "react-native": "./dist/index.react-native.mjs",
+    "types": "./dist/types/index.d.ts",
+    "type": "commonjs",
+    "files": [
+        "./dist/types",
+        "./dist/index.*"
+    ],
+    "sideEffects": false,
+    "keywords": [
+        "solana",
+        "framework",
+        "standard",
+        "specifications",
+        "parsers"
+    ],
+    "scripts": {
+        "build": "rimraf dist && pnpm build:src && pnpm build:types",
+        "build:src": "zx ../../node_modules/@codama/internals/scripts/build-src.mjs package",
+        "build:types": "zx ../../node_modules/@codama/internals/scripts/build-types.mjs",
+        "dev": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs node --watch",
+        "lint": "zx ../../node_modules/@codama/internals/scripts/lint.mjs",
+        "lint:fix": "zx ../../node_modules/@codama/internals/scripts/lint.mjs --fix",
+        "test": "pnpm test:types && pnpm test:treeshakability && pnpm test:browser && pnpm test:node && pnpm test:react-native",
+        "test:browser": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs browser",
+        "test:node": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs node",
+        "test:react-native": "zx ../../node_modules/@codama/internals/scripts/test-unit.mjs react-native",
+        "test:treeshakability": "zx ../../node_modules/@codama/internals/scripts/test-treeshakability.mjs",
+        "test:types": "zx ../../node_modules/@codama/internals/scripts/test-types.mjs"
+    },
+    "dependencies": {
+        "@codama/dynamic-codecs": "workspace:*",
+        "@codama/errors": "workspace:*",
+        "@codama/nodes": "workspace:*",
+        "@codama/visitors-core": "workspace:*",
+        "@solana/instructions": "2.0.0-rc.4"
+    },
+    "devDependencies": {
+        "@solana/codecs": "2.0.0-rc.4"
+    },
+    "license": "MIT",
+    "repository": {
+        "type": "git",
+        "url": "https://github.com/codama-idl/codama"
+    },
+    "bugs": {
+        "url": "http://github.com/codama-idl/codama/issues"
+    },
+    "browserslist": [
+        "supports bigint and not dead",
+        "maintained node versions"
+    ]
+}

+ 83 - 0
packages/dynamic-parsers/src/discriminators.ts

@@ -0,0 +1,83 @@
+import { CodecAndValueVisitors, containsBytes, ReadonlyUint8Array } from '@codama/dynamic-codecs';
+import {
+    CODAMA_ERROR__DISCRIMINATOR_FIELD_HAS_NO_DEFAULT_VALUE,
+    CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND,
+    CodamaError,
+} from '@codama/errors';
+import {
+    assertIsNode,
+    ConstantDiscriminatorNode,
+    constantDiscriminatorNode,
+    constantValueNode,
+    DiscriminatorNode,
+    FieldDiscriminatorNode,
+    isNode,
+    SizeDiscriminatorNode,
+    StructTypeNode,
+} from '@codama/nodes';
+import { visit } from '@codama/visitors-core';
+
+export function matchDiscriminators(
+    bytes: ReadonlyUint8Array,
+    discriminators: DiscriminatorNode[],
+    struct: StructTypeNode,
+    visitors: CodecAndValueVisitors,
+): boolean {
+    return (
+        discriminators.length > 0 &&
+        discriminators.every(discriminator => matchDiscriminator(bytes, discriminator, struct, visitors))
+    );
+}
+
+function matchDiscriminator(
+    bytes: ReadonlyUint8Array,
+    discriminator: DiscriminatorNode,
+    struct: StructTypeNode,
+    visitors: CodecAndValueVisitors,
+): boolean {
+    if (isNode(discriminator, 'constantDiscriminatorNode')) {
+        return matchConstantDiscriminator(bytes, discriminator, visitors);
+    }
+    if (isNode(discriminator, 'fieldDiscriminatorNode')) {
+        return matchFieldDiscriminator(bytes, discriminator, struct, visitors);
+    }
+    assertIsNode(discriminator, 'sizeDiscriminatorNode');
+    return matchSizeDiscriminator(bytes, discriminator);
+}
+
+function matchConstantDiscriminator(
+    bytes: ReadonlyUint8Array,
+    discriminator: ConstantDiscriminatorNode,
+    { codecVisitor, valueVisitor }: CodecAndValueVisitors,
+): boolean {
+    const codec = visit(discriminator.constant.type, codecVisitor);
+    const value = visit(discriminator.constant.value, valueVisitor);
+    const bytesToMatch = codec.encode(value);
+    return containsBytes(bytes, bytesToMatch, discriminator.offset);
+}
+
+function matchFieldDiscriminator(
+    bytes: ReadonlyUint8Array,
+    discriminator: FieldDiscriminatorNode,
+    struct: StructTypeNode,
+    visitors: CodecAndValueVisitors,
+): boolean {
+    const field = struct.fields.find(field => field.name === discriminator.name);
+    if (!field) {
+        throw new CodamaError(CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND, {
+            field: discriminator.name,
+        });
+    }
+    if (!field.defaultValue) {
+        throw new CodamaError(CODAMA_ERROR__DISCRIMINATOR_FIELD_HAS_NO_DEFAULT_VALUE, {
+            field: discriminator.name,
+        });
+    }
+    const constantNode = constantValueNode(field.type, field.defaultValue);
+    const constantDiscriminator = constantDiscriminatorNode(constantNode, discriminator.offset);
+    return matchConstantDiscriminator(bytes, constantDiscriminator, visitors);
+}
+
+function matchSizeDiscriminator(bytes: ReadonlyUint8Array, discriminator: SizeDiscriminatorNode): boolean {
+    return bytes.length === discriminator.size;
+}

+ 96 - 0
packages/dynamic-parsers/src/identify.ts

@@ -0,0 +1,96 @@
+import { CodecAndValueVisitors, getCodecAndValueVisitors, ReadonlyUint8Array } from '@codama/dynamic-codecs';
+import {
+    AccountNode,
+    GetNodeFromKind,
+    InstructionNode,
+    isNodeFilter,
+    resolveNestedTypeNode,
+    RootNode,
+    structTypeNodeFromInstructionArgumentNodes,
+} from '@codama/nodes';
+import {
+    getRecordLinkablesVisitor,
+    LinkableDictionary,
+    NodePath,
+    NodeStack,
+    pipe,
+    recordNodeStackVisitor,
+    visit,
+    Visitor,
+} from '@codama/visitors-core';
+
+import { matchDiscriminators } from './discriminators';
+
+export function identifyAccountData(
+    root: RootNode,
+    bytes: ReadonlyUint8Array | Uint8Array,
+): NodePath<AccountNode> | undefined {
+    return identifyData(root, bytes, 'accountNode');
+}
+
+export function identifyInstructionData(
+    root: RootNode,
+    bytes: ReadonlyUint8Array | Uint8Array,
+): NodePath<InstructionNode> | undefined {
+    return identifyData(root, bytes, 'instructionNode');
+}
+
+export function identifyData<TKind extends 'accountNode' | 'instructionNode'>(
+    root: RootNode,
+    bytes: ReadonlyUint8Array | Uint8Array,
+    kind?: TKind | TKind[],
+): NodePath<GetNodeFromKind<TKind>> | undefined {
+    const stack = new NodeStack();
+    const linkables = new LinkableDictionary();
+    visit(root, getRecordLinkablesVisitor(linkables));
+
+    const codecAndValueVisitors = getCodecAndValueVisitors(linkables, { stack });
+    const visitor = getByteIdentificationVisitor(
+        kind ?? (['accountNode', 'instructionNode'] as TKind[]),
+        bytes,
+        codecAndValueVisitors,
+        { stack },
+    );
+
+    return visit(root, visitor);
+}
+
+export function getByteIdentificationVisitor<TKind extends 'accountNode' | 'instructionNode'>(
+    kind: TKind | TKind[],
+    bytes: ReadonlyUint8Array | Uint8Array,
+    codecAndValueVisitors: CodecAndValueVisitors,
+    options: { stack?: NodeStack } = {},
+) {
+    const stack = options.stack ?? new NodeStack();
+
+    return pipe(
+        {
+            visitAccount(node) {
+                if (!node.discriminators) return;
+                const struct = resolveNestedTypeNode(node.data);
+                const match = matchDiscriminators(bytes, node.discriminators, struct, 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));
+                for (const candidate of candidates) {
+                    const result = visit(candidate, this);
+                    if (result) return result;
+                }
+            },
+            visitRoot(node) {
+                return visit(node.program, this);
+            },
+        } as Visitor<
+            NodePath<GetNodeFromKind<TKind>> | undefined,
+            'accountNode' | 'instructionNode' | 'programNode' | 'rootNode'
+        >,
+        v => recordNodeStackVisitor(v, stack),
+    );
+}

+ 2 - 0
packages/dynamic-parsers/src/index.ts

@@ -0,0 +1,2 @@
+export * from './identify';
+export * from './parsers';

+ 64 - 0
packages/dynamic-parsers/src/parsers.ts

@@ -0,0 +1,64 @@
+import { getNodeCodec, ReadonlyUint8Array } from '@codama/dynamic-codecs';
+import { AccountNode, CamelCaseString, GetNodeFromKind, InstructionNode, RootNode } from '@codama/nodes';
+import { getLastNodeFromPath, NodePath } from '@codama/visitors-core';
+import type {
+    IAccountLookupMeta,
+    IAccountMeta,
+    IInstruction,
+    IInstructionWithAccounts,
+    IInstructionWithData,
+} from '@solana/instructions';
+
+import { identifyData } from './identify';
+
+export type ParsedData<TNode extends AccountNode | InstructionNode> = {
+    data: unknown;
+    path: NodePath<TNode>;
+};
+
+export function parseAccountData(
+    root: RootNode,
+    bytes: ReadonlyUint8Array | Uint8Array,
+): ParsedData<AccountNode> | undefined {
+    return parseData(root, bytes, 'accountNode');
+}
+
+export function parseInstructionData(
+    root: RootNode,
+    bytes: ReadonlyUint8Array | Uint8Array,
+): ParsedData<InstructionNode> | undefined {
+    return parseData(root, bytes, 'instructionNode');
+}
+
+export function parseData<TKind extends 'accountNode' | 'instructionNode'>(
+    root: RootNode,
+    bytes: ReadonlyUint8Array | Uint8Array,
+    kind?: TKind | TKind[],
+): ParsedData<GetNodeFromKind<TKind>> | undefined {
+    const path = identifyData<TKind>(root, bytes, kind ?? (['accountNode', 'instructionNode'] as TKind[]));
+    if (!path) return undefined;
+    const codec = getNodeCodec(path as NodePath<AccountNode | InstructionNode>);
+    const data = codec.decode(bytes);
+    return { data, path };
+}
+
+type ParsedInstructionAccounts = ReadonlyArray<IAccountMeta & { name: CamelCaseString }>;
+type ParsedInstruction = ParsedData<InstructionNode> & { accounts: ParsedInstructionAccounts };
+
+export function parseInstruction(
+    root: RootNode,
+    instruction: IInstruction &
+        IInstructionWithAccounts<readonly (IAccountLookupMeta | IAccountMeta)[]> &
+        IInstructionWithData<Uint8Array>,
+): ParsedInstruction | undefined {
+    const parsedData = parseInstructionData(root, instruction.data);
+    if (!parsedData) return undefined;
+    instruction.accounts;
+    const instructionNode = getLastNodeFromPath(parsedData.path);
+    const accounts: ParsedInstructionAccounts = instructionNode.accounts.flatMap((account, index) => {
+        const accountMeta = instruction.accounts[index];
+        if (!accountMeta) return [];
+        return [{ ...accountMeta, name: account.name }];
+    });
+    return { ...parsedData, accounts };
+}

+ 6 - 0
packages/dynamic-parsers/src/types/global.d.ts

@@ -0,0 +1,6 @@
+declare const __BROWSER__: boolean;
+declare const __ESM__: boolean;
+declare const __NODEJS__: boolean;
+declare const __REACTNATIVE__: boolean;
+declare const __TEST__: boolean;
+declare const __VERSION__: string;

+ 5 - 0
packages/dynamic-parsers/test/_setup.ts

@@ -0,0 +1,5 @@
+import { getBase16Encoder, ReadonlyUint8Array } from '@solana/codecs';
+
+export function hex(hexadecimal: string): ReadonlyUint8Array {
+    return getBase16Encoder().encode(hexadecimal);
+}

+ 327 - 0
packages/dynamic-parsers/test/discriminators.test.ts

@@ -0,0 +1,327 @@
+import { CodecAndValueVisitors, getCodecAndValueVisitors } from '@codama/dynamic-codecs';
+import {
+    CODAMA_ERROR__DISCRIMINATOR_FIELD_HAS_NO_DEFAULT_VALUE,
+    CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND,
+    CodamaError,
+} from '@codama/errors';
+import {
+    accountNode,
+    constantDiscriminatorNode,
+    constantValueNode,
+    constantValueNodeFromBytes,
+    definedTypeLinkNode,
+    definedTypeNode,
+    fieldDiscriminatorNode,
+    fixedSizeTypeNode,
+    numberTypeNode,
+    numberValueNode,
+    programLinkNode,
+    programNode,
+    rootNode,
+    sizeDiscriminatorNode,
+    stringTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+} from '@codama/nodes';
+import { getRecordLinkablesVisitor, LinkableDictionary, NodeStack, visit } from '@codama/visitors-core';
+import { beforeEach, describe, expect, test } from 'vitest';
+
+import { matchDiscriminators } from '../src/discriminators';
+import { hex } from './_setup';
+
+describe('matchDiscriminators', () => {
+    let linkables: LinkableDictionary;
+    let codecAndValueVisitors: CodecAndValueVisitors;
+    beforeEach(() => {
+        linkables = new LinkableDictionary();
+        codecAndValueVisitors = getCodecAndValueVisitors(linkables);
+    });
+    test('it does not match if no discriminators are provided', () => {
+        const result = matchDiscriminators(hex('ff'), [], structTypeNode([]), codecAndValueVisitors);
+        expect(result).toBe(false);
+    });
+    describe('size discriminators', () => {
+        test('it returns true if the size matches exactly', () => {
+            const result = matchDiscriminators(
+                hex('0102030405'),
+                [sizeDiscriminatorNode(5)],
+                structTypeNode([]),
+                codecAndValueVisitors,
+            );
+            expect(result).toBe(true);
+        });
+        test('it returns false if the size is lower', () => {
+            const result = matchDiscriminators(
+                hex('01020304'),
+                [sizeDiscriminatorNode(5)],
+                structTypeNode([]),
+                codecAndValueVisitors,
+            );
+            expect(result).toBe(false);
+        });
+        test('it returns false if the size is greater', () => {
+            const result = matchDiscriminators(
+                hex('010203040506'),
+                [sizeDiscriminatorNode(5)],
+                structTypeNode([]),
+                codecAndValueVisitors,
+            );
+            expect(result).toBe(false);
+        });
+    });
+    describe('constant discriminators', () => {
+        test('it returns true if the bytes start with the provided constant', () => {
+            const discriminator = constantDiscriminatorNode(constantValueNodeFromBytes('base16', 'ff'));
+            const result = matchDiscriminators(
+                hex('ff0102030405'),
+                [discriminator],
+                structTypeNode([]),
+                codecAndValueVisitors,
+            );
+            expect(result).toBe(true);
+        });
+        test('it returns false if the bytes do not start with the provided constant', () => {
+            const discriminator = constantDiscriminatorNode(constantValueNodeFromBytes('base16', 'ff'));
+            const result = matchDiscriminators(
+                hex('aa0102030405'),
+                [discriminator],
+                structTypeNode([]),
+                codecAndValueVisitors,
+            );
+            expect(result).toBe(false);
+        });
+        test('it returns true if the bytes match with the provided constant at the given offset', () => {
+            const discriminator = constantDiscriminatorNode(
+                constantValueNodeFromBytes('base16', 'ff'),
+                3 /** offset */,
+            );
+            const result = matchDiscriminators(
+                hex('010203ff0405'),
+                [discriminator],
+                structTypeNode([]),
+                codecAndValueVisitors,
+            );
+            expect(result).toBe(true);
+        });
+        test('it returns false if the bytes do not match with the provided constant at the given offset', () => {
+            const discriminator = constantDiscriminatorNode(
+                constantValueNodeFromBytes('base16', 'ff'),
+                3 /** offset */,
+            );
+            const result = matchDiscriminators(
+                hex('010203aa0405'),
+                [discriminator],
+                structTypeNode([]),
+                codecAndValueVisitors,
+            );
+            expect(result).toBe(false);
+        });
+        test('it resolves link nodes correctly', () => {
+            // Given two link nodes designed so that the path would
+            // fail if we did not save and restored linked paths.
+            const discriminator = constantDiscriminatorNode(
+                constantValueNode(definedTypeLinkNode('typeB1', programLinkNode('programB')), numberValueNode(42)),
+            );
+            const programA = programNode({
+                accounts: [accountNode({ discriminators: [discriminator], name: 'myAccount' })],
+                definedTypes: [
+                    definedTypeNode({
+                        name: 'typeA',
+                        type: definedTypeLinkNode('typeB1', programLinkNode('programB')),
+                    }),
+                ],
+                name: 'programA',
+                publicKey: '1111',
+            });
+            const programB = programNode({
+                definedTypes: [
+                    definedTypeNode({ name: 'typeB1', type: definedTypeLinkNode('typeB2') }),
+                    definedTypeNode({ name: 'typeB2', type: numberTypeNode('u32') }),
+                ],
+                name: 'programB',
+                publicKey: '2222',
+            });
+            const root = rootNode(programA, [programB]);
+
+            // And given a recorded linkables dictionary.
+            const linkables = new LinkableDictionary();
+            visit(root, getRecordLinkablesVisitor(linkables));
+
+            // And a stack keeping track of the current visited nodes.
+            const stack = new NodeStack([root, programA, programA.accounts[0]]);
+            codecAndValueVisitors = getCodecAndValueVisitors(linkables, { stack });
+
+            // When we match the discriminator which should resolve to a u32 number equal to 42.
+            const result = matchDiscriminators(
+                hex('2a0000000102030405'),
+                [discriminator],
+                structTypeNode([]),
+                codecAndValueVisitors,
+            );
+
+            // Then we expect the discriminator to match.
+            expect(result).toBe(true);
+        });
+    });
+    describe('field discriminators', () => {
+        test('it returns true if the bytes start with the provided field default value', () => {
+            const discriminator = fieldDiscriminatorNode('key');
+            const fields = structTypeNode([
+                structFieldTypeNode({
+                    defaultValue: numberValueNode(0xff),
+                    name: 'key',
+                    type: numberTypeNode('u8'),
+                }),
+            ]);
+            const result = matchDiscriminators(hex('ff0102030405'), [discriminator], fields, codecAndValueVisitors);
+            expect(result).toBe(true);
+        });
+        test('it returns false if the bytes do not start with the provided field default value', () => {
+            const discriminator = fieldDiscriminatorNode('key');
+            const fields = structTypeNode([
+                structFieldTypeNode({
+                    defaultValue: numberValueNode(0xff),
+                    name: 'key',
+                    type: numberTypeNode('u8'),
+                }),
+            ]);
+            const result = matchDiscriminators(hex('aa0102030405'), [discriminator], fields, codecAndValueVisitors);
+            expect(result).toBe(false);
+        });
+        test('it returns true if the bytes match with the provided field default value at the given offset', () => {
+            const discriminator = fieldDiscriminatorNode('key', 3 /** offset */);
+            const fields = structTypeNode([
+                structFieldTypeNode({ name: 'id', type: fixedSizeTypeNode(stringTypeNode('utf8'), 3) }),
+                structFieldTypeNode({
+                    defaultValue: numberValueNode(0xff),
+                    name: 'key',
+                    type: numberTypeNode('u8'),
+                }),
+            ]);
+            const result = matchDiscriminators(hex('010203ff0405'), [discriminator], fields, codecAndValueVisitors);
+            expect(result).toBe(true);
+        });
+        test('it returns false if the bytes do not match with the provided field default value at the given offset', () => {
+            const discriminator = fieldDiscriminatorNode('key', 3 /** offset */);
+            const fields = structTypeNode([
+                structFieldTypeNode({ name: 'id', type: fixedSizeTypeNode(stringTypeNode('utf8'), 3) }),
+                structFieldTypeNode({
+                    defaultValue: numberValueNode(0xff),
+                    name: 'key',
+                    type: numberTypeNode('u8'),
+                }),
+            ]);
+            const result = matchDiscriminators(hex('010203aa0405'), [discriminator], fields, codecAndValueVisitors);
+            expect(result).toBe(false);
+        });
+        test('it throws an error if the discriminator field is not found', () => {
+            const discriminator = fieldDiscriminatorNode('key');
+            const fields = structTypeNode([]);
+            expect(() =>
+                matchDiscriminators(hex('0102030405'), [discriminator], fields, codecAndValueVisitors),
+            ).toThrow(new CodamaError(CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND, { field: 'key' }));
+        });
+        test('it throws an error if the discriminator field does not have a default value', () => {
+            const discriminator = fieldDiscriminatorNode('key');
+            const fields = structTypeNode([
+                structFieldTypeNode({
+                    name: 'key',
+                    type: numberTypeNode('u8'),
+                }),
+            ]);
+            expect(() =>
+                matchDiscriminators(hex('0102030405'), [discriminator], fields, codecAndValueVisitors),
+            ).toThrow(new CodamaError(CODAMA_ERROR__DISCRIMINATOR_FIELD_HAS_NO_DEFAULT_VALUE, { field: 'key' }));
+        });
+        test('it resolves link nodes correctly', () => {
+            // Given two link nodes designed so that the path would
+            // fail if we did not save and restored linked paths.
+            const discriminator = fieldDiscriminatorNode('key');
+            const fields = structTypeNode([
+                structFieldTypeNode({
+                    defaultValue: numberValueNode(42),
+                    name: 'key',
+                    type: definedTypeLinkNode('typeB1', programLinkNode('programB')),
+                }),
+            ]);
+            const programA = programNode({
+                accounts: [accountNode({ data: fields, discriminators: [discriminator], name: 'myAccount' })],
+                definedTypes: [
+                    definedTypeNode({
+                        name: 'typeA',
+                        type: definedTypeLinkNode('typeB1', programLinkNode('programB')),
+                    }),
+                ],
+                name: 'programA',
+                publicKey: '1111',
+            });
+            const programB = programNode({
+                definedTypes: [
+                    definedTypeNode({ name: 'typeB1', type: definedTypeLinkNode('typeB2') }),
+                    definedTypeNode({ name: 'typeB2', type: numberTypeNode('u32') }),
+                ],
+                name: 'programB',
+                publicKey: '2222',
+            });
+            const root = rootNode(programA, [programB]);
+
+            // And given a recorded linkables dictionary.
+            const linkables = new LinkableDictionary();
+            visit(root, getRecordLinkablesVisitor(linkables));
+
+            // And a stack keeping track of the current visited nodes.
+            const stack = new NodeStack([root, programA, programA.accounts[0]]);
+            codecAndValueVisitors = getCodecAndValueVisitors(linkables, { stack });
+
+            // When we match the discriminator which should resolve to a u32 number equal to 42.
+            const result = matchDiscriminators(
+                hex('2a0000000102030405'),
+                [discriminator],
+                fields,
+                codecAndValueVisitors,
+            );
+
+            // Then we expect the discriminator to match.
+            expect(result).toBe(true);
+        });
+    });
+    describe('multiple discriminators', () => {
+        test('it returns true if all discriminators match', () => {
+            const result = matchDiscriminators(
+                hex('ff0102030405'),
+                [constantDiscriminatorNode(constantValueNodeFromBytes('base16', 'ff')), sizeDiscriminatorNode(6)],
+                structTypeNode([]),
+                codecAndValueVisitors,
+            );
+            expect(result).toBe(true);
+        });
+        test('it returns false if any discriminator does not match', () => {
+            const result = matchDiscriminators(
+                hex('ff0102030405'),
+                [constantDiscriminatorNode(constantValueNodeFromBytes('base16', 'ff')), sizeDiscriminatorNode(999)],
+                structTypeNode([]),
+                codecAndValueVisitors,
+            );
+            expect(result).toBe(false);
+        });
+        test('it can match on all discriminator types', () => {
+            const result = matchDiscriminators(
+                hex('aabb01020304'),
+                [
+                    fieldDiscriminatorNode('key'),
+                    constantDiscriminatorNode(constantValueNodeFromBytes('base16', 'bb'), 1),
+                    sizeDiscriminatorNode(6),
+                ],
+                structTypeNode([
+                    structFieldTypeNode({
+                        defaultValue: numberValueNode(0xaa),
+                        name: 'key',
+                        type: numberTypeNode('u8'),
+                    }),
+                ]),
+                codecAndValueVisitors,
+            );
+            expect(result).toBe(true);
+        });
+    });
+});

+ 167 - 0
packages/dynamic-parsers/test/identify.test.ts

@@ -0,0 +1,167 @@
+import {
+    accountNode,
+    constantDiscriminatorNode,
+    constantValueNodeFromBytes,
+    instructionNode,
+    programNode,
+    rootNode,
+    sizeDiscriminatorNode,
+} from '@codama/nodes';
+import { describe, expect, test } from 'vitest';
+
+import { identifyAccountData, identifyInstructionData } from '../src';
+import { hex } from './_setup';
+
+describe('identifyAccountData', () => {
+    test('it identifies an account using its discriminator nodes', () => {
+        const root = rootNode(
+            programNode({
+                accounts: [accountNode({ discriminators: [sizeDiscriminatorNode(4)], name: 'myAccount' })],
+                name: 'myProgram',
+                publicKey: '1111',
+            }),
+        );
+        const result = identifyAccountData(root, hex('01020304'));
+        expect(result).toStrictEqual([root, root.program, root.program.accounts[0]]);
+    });
+    test('it fails to identify accounts whose discriminator nodes do not match the given data', () => {
+        const root = rootNode(
+            programNode({
+                accounts: [accountNode({ discriminators: [sizeDiscriminatorNode(999)], name: 'myAccount' })],
+                name: 'myProgram',
+                publicKey: '1111',
+            }),
+        );
+        const result = identifyAccountData(root, hex('01020304'));
+        expect(result).toBeUndefined();
+    });
+    test('it fails to identify accounts with no discriminator nodes', () => {
+        const root = rootNode(
+            programNode({ accounts: [accountNode({ name: 'myAccount' })], name: 'myProgram', publicKey: '1111' }),
+        );
+        const result = identifyAccountData(root, hex('01020304'));
+        expect(result).toBeUndefined();
+    });
+    test('it identifies the first matching account if multiple accounts match', () => {
+        const root = rootNode(
+            programNode({
+                accounts: [
+                    accountNode({
+                        discriminators: [sizeDiscriminatorNode(4)],
+                        name: 'accountA',
+                    }),
+                    accountNode({
+                        discriminators: [constantDiscriminatorNode(constantValueNodeFromBytes('base16', 'ff'))],
+                        name: 'accountB',
+                    }),
+                ],
+                name: 'myProgram',
+                publicKey: '1111',
+            }),
+        );
+        const result = identifyAccountData(root, hex('ff010203'));
+        expect(result).toStrictEqual([root, root.program, root.program.accounts[0]]);
+    });
+    test('it does not identify accounts in additional programs', () => {
+        const root = rootNode(programNode({ name: 'myProgram', publicKey: '1111' }), [
+            programNode({
+                accounts: [accountNode({ discriminators: [sizeDiscriminatorNode(4)], name: 'myAccount' })],
+                name: 'myProgram',
+                publicKey: '1111',
+            }),
+        ]);
+        const result = identifyAccountData(root, hex('01020304'));
+        expect(result).toBeUndefined();
+    });
+    test('it does not identify accounts using instruction discriminators', () => {
+        const root = rootNode(programNode({ name: 'myProgram', publicKey: '1111' }), [
+            programNode({
+                instructions: [instructionNode({ discriminators: [sizeDiscriminatorNode(4)], name: 'myInstruction' })],
+                name: 'myProgram',
+                publicKey: '1111',
+            }),
+        ]);
+        const result = identifyAccountData(root, hex('01020304'));
+        expect(result).toBeUndefined();
+    });
+});
+
+describe('identifyInstructionData', () => {
+    test('it identifies an instruction using its discriminator nodes', () => {
+        const root = rootNode(
+            programNode({
+                instructions: [instructionNode({ discriminators: [sizeDiscriminatorNode(4)], name: 'myInstruction' })],
+                name: 'myProgram',
+                publicKey: '1111',
+            }),
+        );
+        const result = identifyInstructionData(root, hex('01020304'));
+        expect(result).toStrictEqual([root, root.program, root.program.instructions[0]]);
+    });
+    test('it fails to identify instructions whose discriminator nodes do not match the given data', () => {
+        const root = rootNode(
+            programNode({
+                instructions: [
+                    instructionNode({ discriminators: [sizeDiscriminatorNode(999)], name: 'myInstruction' }),
+                ],
+                name: 'myProgram',
+                publicKey: '1111',
+            }),
+        );
+        const result = identifyInstructionData(root, hex('01020304'));
+        expect(result).toBeUndefined();
+    });
+    test('it fails to identify instructions with no discriminator nodes', () => {
+        const root = rootNode(
+            programNode({
+                instructions: [instructionNode({ name: 'myInstruction' })],
+                name: 'myProgram',
+                publicKey: '1111',
+            }),
+        );
+        const result = identifyInstructionData(root, hex('01020304'));
+        expect(result).toBeUndefined();
+    });
+    test('it identifies the first matching instruction if multiple instructions match', () => {
+        const root = rootNode(
+            programNode({
+                instructions: [
+                    instructionNode({
+                        discriminators: [sizeDiscriminatorNode(4)],
+                        name: 'instructionA',
+                    }),
+                    instructionNode({
+                        discriminators: [constantDiscriminatorNode(constantValueNodeFromBytes('base16', 'ff'))],
+                        name: 'instructionB',
+                    }),
+                ],
+                name: 'myProgram',
+                publicKey: '1111',
+            }),
+        );
+        const result = identifyInstructionData(root, hex('ff010203'));
+        expect(result).toStrictEqual([root, root.program, root.program.instructions[0]]);
+    });
+    test('it does not identify instructions in additional programs', () => {
+        const root = rootNode(programNode({ name: 'myProgram', publicKey: '1111' }), [
+            programNode({
+                instructions: [instructionNode({ discriminators: [sizeDiscriminatorNode(4)], name: 'myInstruction' })],
+                name: 'myProgram',
+                publicKey: '1111',
+            }),
+        ]);
+        const result = identifyInstructionData(root, hex('01020304'));
+        expect(result).toBeUndefined();
+    });
+    test('it does not identify instructions using account discriminators', () => {
+        const root = rootNode(programNode({ name: 'myProgram', publicKey: '1111' }), [
+            programNode({
+                accounts: [accountNode({ discriminators: [sizeDiscriminatorNode(4)], name: 'myAccount' })],
+                name: 'myProgram',
+                publicKey: '1111',
+            }),
+        ]);
+        const result = identifyInstructionData(root, hex('01020304'));
+        expect(result).toBeUndefined();
+    });
+});

+ 92 - 0
packages/dynamic-parsers/test/parsers.test.ts

@@ -0,0 +1,92 @@
+import {
+    accountNode,
+    fieldDiscriminatorNode,
+    instructionArgumentNode,
+    instructionNode,
+    numberTypeNode,
+    numberValueNode,
+    programNode,
+    rootNode,
+    sizePrefixTypeNode,
+    stringTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+} from '@codama/nodes';
+import { describe, expect, test } from 'vitest';
+
+import { parseAccountData, parseInstructionData } from '../src';
+import { hex } from './_setup';
+
+describe('parseAccountData', () => {
+    test('it parses some account data from a root node', () => {
+        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 = parseAccountData(root, hex('090500416c6963652a'));
+        expect(result).toStrictEqual({
+            data: { age: 42, discriminator: 9, firstname: 'Alice' },
+            path: [root, root.program, root.program.accounts[0]],
+        });
+    });
+});
+
+describe('parseInstructionData', () => {
+    test('it parses some instruction data from a root node', () => {
+        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 = parseInstructionData(root, hex('090500416c6963652a'));
+        expect(result).toStrictEqual({
+            data: { age: 42, discriminator: 9, firstname: 'Alice' },
+            path: [root, root.program, root.program.instructions[0]],
+        });
+    });
+});

+ 6 - 0
packages/dynamic-parsers/test/types/global.d.ts

@@ -0,0 +1,6 @@
+declare const __BROWSER__: boolean;
+declare const __ESM__: boolean;
+declare const __NODEJS__: boolean;
+declare const __REACTNATIVE__: boolean;
+declare const __TEST__: boolean;
+declare const __VERSION__: string;

+ 10 - 0
packages/dynamic-parsers/tsconfig.declarations.json

@@ -0,0 +1,10 @@
+{
+    "compilerOptions": {
+        "declaration": true,
+        "declarationMap": true,
+        "emitDeclarationOnly": true,
+        "outDir": "./dist/types"
+    },
+    "extends": "./tsconfig.json",
+    "include": ["src/index.ts", "src/types"]
+}

+ 7 - 0
packages/dynamic-parsers/tsconfig.json

@@ -0,0 +1,7 @@
+{
+    "$schema": "https://json.schemastore.org/tsconfig",
+    "compilerOptions": { "lib": [] },
+    "display": "@codama/dynamic-parsers",
+    "extends": "../internals/tsconfig.base.json",
+    "include": ["src", "test"]
+}

+ 4 - 0
packages/errors/src/codes.ts

@@ -34,6 +34,8 @@ export const CODAMA_ERROR__VERSION_MISMATCH = 6 as const;
 export const CODAMA_ERROR__UNRECOGNIZED_NUMBER_FORMAT = 7 as const;
 export const CODAMA_ERROR__UNRECOGNIZED_BYTES_ENCODING = 8 as const;
 export const CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND = 9 as const;
+export const CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND = 10 as const;
+export const CODAMA_ERROR__DISCRIMINATOR_FIELD_HAS_NO_DEFAULT_VALUE = 11 as const;
 
 // Visitors-related errors.
 // Reserve error codes in the range [1200000-1200999].
@@ -85,6 +87,8 @@ export type CodamaErrorCode =
     | typeof CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED
     | typeof CODAMA_ERROR__ANCHOR__TYPE_PATH_MISSING
     | typeof CODAMA_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE
+    | typeof CODAMA_ERROR__DISCRIMINATOR_FIELD_HAS_NO_DEFAULT_VALUE
+    | typeof CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND
     | typeof CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND
     | typeof CODAMA_ERROR__LINKED_NODE_NOT_FOUND
     | typeof CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE

+ 8 - 0
packages/errors/src/context.ts

@@ -25,6 +25,8 @@ import {
     CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED,
     CODAMA_ERROR__ANCHOR__TYPE_PATH_MISSING,
     CODAMA_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE,
+    CODAMA_ERROR__DISCRIMINATOR_FIELD_HAS_NO_DEFAULT_VALUE,
+    CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND,
     CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND,
     CODAMA_ERROR__LINKED_NODE_NOT_FOUND,
     CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE,
@@ -76,6 +78,12 @@ export type CodamaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
     [CODAMA_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE]: {
         idlType: string;
     };
+    [CODAMA_ERROR__DISCRIMINATOR_FIELD_HAS_NO_DEFAULT_VALUE]: {
+        field: CamelCaseString;
+    };
+    [CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND]: {
+        field: CamelCaseString;
+    };
     [CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND]: {
         enum: EnumTypeNode;
         enumName: CamelCaseString;

+ 4 - 0
packages/errors/src/messages.ts

@@ -9,6 +9,8 @@ import {
     CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED,
     CODAMA_ERROR__ANCHOR__TYPE_PATH_MISSING,
     CODAMA_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE,
+    CODAMA_ERROR__DISCRIMINATOR_FIELD_HAS_NO_DEFAULT_VALUE,
+    CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND,
     CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND,
     CODAMA_ERROR__LINKED_NODE_NOT_FOUND,
     CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE,
@@ -49,6 +51,8 @@ export const CodamaErrorMessages: Readonly<{
     [CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED]: 'Seed kind [$kind] is not implemented.',
     [CODAMA_ERROR__ANCHOR__TYPE_PATH_MISSING]: 'Field type is missing for path [$path] in [$idlType].',
     [CODAMA_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE]: 'Unrecognized Anchor IDL type [$idlType].',
+    [CODAMA_ERROR__DISCRIMINATOR_FIELD_HAS_NO_DEFAULT_VALUE]: 'Discriminator field [$field] has no default value.',
+    [CODAMA_ERROR__DISCRIMINATOR_FIELD_NOT_FOUND]: 'Could not find discriminator field [$field]',
     [CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND]: 'Enum variant [$variant] not found in enum type [$enumName].',
     [CODAMA_ERROR__LINKED_NODE_NOT_FOUND]: 'Could not find linked node [$name] from [$kind].',
     [CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE]:

+ 33 - 0
pnpm-lock.yaml

@@ -90,6 +90,28 @@ importers:
         specifier: 2.0.0-rc.4
         version: 2.0.0-rc.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)
 
+  packages/dynamic-parsers:
+    dependencies:
+      '@codama/dynamic-codecs':
+        specifier: workspace:*
+        version: link:../dynamic-codecs
+      '@codama/errors':
+        specifier: workspace:*
+        version: link:../errors
+      '@codama/nodes':
+        specifier: workspace:*
+        version: link:../nodes
+      '@codama/visitors-core':
+        specifier: workspace:*
+        version: link:../visitors-core
+      '@solana/instructions':
+        specifier: 2.0.0-rc.4
+        version: 2.0.0-rc.4(typescript@5.6.3)
+    devDependencies:
+      '@solana/codecs':
+        specifier: 2.0.0-rc.4
+        version: 2.0.0-rc.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)
+
   packages/errors:
     dependencies:
       '@codama/node-types':
@@ -882,6 +904,12 @@ packages:
       eslint-plugin-typescript-sort-keys: ^3.2.0
       typescript: ^5.1.6
 
+  '@solana/instructions@2.0.0-rc.4':
+    resolution: {integrity: sha512-k4KcNfrWQX5Zhij9mn6d10MnHEP4YD9qsG8SQHWTELbyoZt3UjzfzG2aY88ao3VDWz3S9JbFIzA11wjJ0TYFxg==}
+    engines: {node: '>=20.18.0'}
+    peerDependencies:
+      typescript: '>=5'
+
   '@solana/options@2.0.0-rc.4':
     resolution: {integrity: sha512-5W8aswMBhcdv2pD5lHLdHIZ98ymhQNBmeFncEoVZLTrshf7KqyxZ8xtILcWNCUgOev1+yp9hMTNV9SEgrgyNrQ==}
     engines: {node: '>=20.18.0'}
@@ -2924,6 +2952,11 @@ snapshots:
       eslint-plugin-typescript-sort-keys: 3.3.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)
       typescript: 5.6.3
 
+  '@solana/instructions@2.0.0-rc.4(typescript@5.6.3)':
+    dependencies:
+      '@solana/errors': 2.0.0-rc.4(typescript@5.6.3)
+      typescript: 5.6.3
+
   '@solana/options@2.0.0-rc.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)':
     dependencies:
       '@solana/codecs-core': 2.0.0-rc.4(typescript@5.6.3)