Browse Source

Add dynamic-codecs package (#289)

Loris Leiva 1 năm trước cách đây
mục cha
commit
9b8b795df2
59 tập tin đã thay đổi với 1989 bổ sung3 xóa
  1. 5 0
      .changeset/witty-wasps-look.md
  2. 1 0
      packages/dynamic-codecs/.gitignore
  3. 5 0
      packages/dynamic-codecs/.prettierignore
  4. 22 0
      packages/dynamic-codecs/LICENSE
  5. 117 0
      packages/dynamic-codecs/README.md
  6. 70 0
      packages/dynamic-codecs/package.json
  7. 419 0
      packages/dynamic-codecs/src/codecs.ts
  8. 2 0
      packages/dynamic-codecs/src/index.ts
  9. 6 0
      packages/dynamic-codecs/src/types/global.d.ts
  10. 106 0
      packages/dynamic-codecs/src/values.ts
  11. 5 0
      packages/dynamic-codecs/test/_setup.ts
  12. 21 0
      packages/dynamic-codecs/test/codecs/AccountNode.test.ts
  13. 23 0
      packages/dynamic-codecs/test/codecs/ArrayTypeNode.test.ts
  14. 21 0
      packages/dynamic-codecs/test/codecs/BooleanTypeNode.test.ts
  15. 23 0
      packages/dynamic-codecs/test/codecs/BytesTypeNode.test.ts
  16. 64 0
      packages/dynamic-codecs/test/codecs/DefinedTypeLinkNode.test.ts
  17. 11 0
      packages/dynamic-codecs/test/codecs/DefinedTypeNode.test.ts
  18. 79 0
      packages/dynamic-codecs/test/codecs/EnumTypeNode.test.ts
  19. 13 0
      packages/dynamic-codecs/test/codecs/FixedSizeTypeNode.test.ts
  20. 22 0
      packages/dynamic-codecs/test/codecs/HiddenPrefixTypeNode.test.ts
  21. 22 0
      packages/dynamic-codecs/test/codecs/HiddenSuffixTypeNode.test.ts
  22. 16 0
      packages/dynamic-codecs/test/codecs/InstructionArgumentNode.test.ts
  23. 21 0
      packages/dynamic-codecs/test/codecs/InstructionNode.test.ts
  24. 43 0
      packages/dynamic-codecs/test/codecs/MapTypeNode.test.ts
  25. 85 0
      packages/dynamic-codecs/test/codecs/NumberTypeNode.test.ts
  26. 29 0
      packages/dynamic-codecs/test/codecs/OptionTypeNode.test.ts
  27. 42 0
      packages/dynamic-codecs/test/codecs/PostOffsetTypeNode.test.ts
  28. 32 0
      packages/dynamic-codecs/test/codecs/PreOffsetTypeNode.test.ts
  29. 15 0
      packages/dynamic-codecs/test/codecs/PublicKeyTypeNode.test.ts
  30. 13 0
      packages/dynamic-codecs/test/codecs/RemainderOptionTypeNode.test.ts
  31. 12 0
      packages/dynamic-codecs/test/codecs/SentinelTypeNode.test.ts
  32. 23 0
      packages/dynamic-codecs/test/codecs/SetTypeNode.test.ts
  33. 11 0
      packages/dynamic-codecs/test/codecs/SizePrefixTypeNode.test.ts
  34. 29 0
      packages/dynamic-codecs/test/codecs/StringTypeNode.test.ts
  35. 11 0
      packages/dynamic-codecs/test/codecs/StructFieldTypeNode.test.ts
  36. 17 0
      packages/dynamic-codecs/test/codecs/StructTypeNode.test.ts
  37. 11 0
      packages/dynamic-codecs/test/codecs/TupleTypeNode.test.ts
  38. 22 0
      packages/dynamic-codecs/test/codecs/ZeroableOptionTypeNode.test.ts
  39. 6 0
      packages/dynamic-codecs/test/types/global.d.ts
  40. 11 0
      packages/dynamic-codecs/test/values/ArrayValueNode.test.ts
  41. 11 0
      packages/dynamic-codecs/test/values/BytesValueNode.test.ts
  42. 91 0
      packages/dynamic-codecs/test/values/ConstantValueNode.test.ts
  43. 106 0
      packages/dynamic-codecs/test/values/EnumValueNode.test.ts
  44. 15 0
      packages/dynamic-codecs/test/values/MapValueNode.test.ts
  45. 10 0
      packages/dynamic-codecs/test/values/NoneValueNode.test.ts
  46. 10 0
      packages/dynamic-codecs/test/values/NumberValueNode.test.ts
  47. 11 0
      packages/dynamic-codecs/test/values/PublicKeyValueNode.test.ts
  48. 11 0
      packages/dynamic-codecs/test/values/SetValueNode.test.ts
  49. 10 0
      packages/dynamic-codecs/test/values/SomeValueNode.test.ts
  50. 10 0
      packages/dynamic-codecs/test/values/StringValueNode.test.ts
  51. 14 0
      packages/dynamic-codecs/test/values/StructValueNode.test.ts
  52. 11 0
      packages/dynamic-codecs/test/values/TupleValueNode.test.ts
  53. 10 0
      packages/dynamic-codecs/tsconfig.declarations.json
  54. 7 0
      packages/dynamic-codecs/tsconfig.json
  55. 8 0
      packages/errors/src/codes.ts
  56. 19 0
      packages/errors/src/context.ts
  57. 8 0
      packages/errors/src/messages.ts
  58. 5 3
      packages/visitors-core/src/NodeStack.ts
  59. 116 0
      pnpm-lock.yaml

+ 5 - 0
.changeset/witty-wasps-look.md

@@ -0,0 +1,5 @@
+---
+'@codama/dynamic-codecs': minor
+---
+
+Add new `dynamic-codecs` package to create `Codecs` from nodes on demand

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

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

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

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

+ 22 - 0
packages/dynamic-codecs/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.

+ 117 - 0
packages/dynamic-codecs/README.md

@@ -0,0 +1,117 @@
+# Codama ➤ Dynamic Codecs
+
+[![npm][npm-image]][npm-url]
+[![npm-downloads][npm-downloads-image]][npm-url]
+
+[npm-downloads-image]: https://img.shields.io/npm/dm/@codama/dynamic-codecs.svg?style=flat
+[npm-image]: https://img.shields.io/npm/v/@codama/dynamic-codecs.svg?style=flat&label=%40codama%2Fdynamic-codecs
+[npm-url]: https://www.npmjs.com/package/@codama/dynamic-codecs
+
+This package provides a set of helpers that provide `Codecs` for Codama nodes that describe data.
+
+## Installation
+
+```sh
+pnpm install @codama/dynamic-codecs
+```
+
+> [!NOTE]
+> This package is **not** included in the main [`codama`](../library) package.
+
+## Functions
+
+### `getNodeCodec(path, options?)`
+
+Given the full `NodePath` of a node inside a Codama IDL, returns a `Codec<unknown>` (as defined in `@solana/codecs`) that enables encoding and decoding data for that node.
+
+```ts
+const codec = getNodeCodec([root, program, definedType]);
+const bytes = codec.encode(someData);
+const decodedData = codec.decode(bytes);
+```
+
+Note that it is important to provide the full `NodePath` of the node in order to properly follow link nodes inside the Codama IDL. Here is a more complex example illustrating how link nodes are resolved:
+
+```ts
+// Here we define a program with two types, one of which is a link to the other.
+const root = rootNode(
+    programNode({
+        definedTypes: [
+            definedTypeNode({ name: 'slot', type: numberTypeNode('u64') }),
+            definedTypeNode({ name: 'lastSlot', type: definedTypeLinkNode('slot') }),
+        ],
+        name: 'myProgram',
+        publicKey: '1111',
+    }),
+);
+
+// The codec for the linked `lastSlot` defined type is resolved using the `slot` defined type.
+const codec = getNodeCodec([root, root.program, root.program.definedTypes[1]]);
+expect(codec.encode(42)).toStrictEqual(hex('2a00000000000000'));
+expect(codec.decode(hex('2a00000000000000'))).toBe(42n);
+```
+
+#### Options
+
+The `getNodeCodec` function accepts the following options.
+
+| Name            | Type            | Default    | Description                                              |
+| --------------- | --------------- | ---------- | -------------------------------------------------------- |
+| `bytesEncoding` | `BytesEncoding` | `"base64"` | The default encoding to use when formatting plain bytes. |
+
+#### Decoded format
+
+In the table below, we illustrate the format of each codec based on the node from which it was created.
+
+Note that we purposefully avoid types such as `Uint8Array`, `Set` or `Map` in order to keep the format JSON compatible. For instance, plain bytes are not provided as `Uint8Array` but as a tuple of type `[BytesEncoding, string]` — e.g. `["base64", "HelloWorld++"]` — where the default bytes encoding is `base64` which is configurable via the `bytesEncoding` option.
+
+| Node                                                                                    | Example                                                     | Notes                                                                                       |
+| --------------------------------------------------------------------------------------- | ----------------------------------------------------------- | ------------------------------------------------------------------------------------------- |
+| [`AccountLinkNode`](../nodes/docs/linkNodes/AccountLinkNode.md)                         | -                                                           | Same as `AccountNode`                                                                       |
+| [`AccountNode`](../nodes/docs/AccountNode.md)                                           | -                                                           | Same as `node.data`                                                                         |
+| [`DefinedTypeLinkNode`](../nodes/docs/linkNodes/DefinedTypeLinkNode.md)                 | -                                                           | Same as `DefinedTypeNode`                                                                   |
+| [`DefinedTypeNode`](../nodes/docs/DefinedTypeNode.md)                                   | -                                                           | Same as `node.type`                                                                         |
+| [`InstructionArgumentLinkNode`](../nodes/docs/linkNodes/InstructionArgumentLinkNode.md) | -                                                           | Same as `InstructionArgumentNode`                                                           |
+| [`InstructionArgumentNode`](../nodes/docs/InstructionArgumentNode.md)                   | -                                                           | Same as `node.type`                                                                         |
+| [`InstructionLinkNode`](../nodes/docs/linkNodes/InstructionLinkNode.md)                 | -                                                           | Same as `InstructionNode`                                                                   |
+| [`InstructionNode`](../nodes/docs/InstructionNode.md)                                   | -                                                           | Same as a `StructTypeNode` containing all `node.arguments`                                  |
+| [`AmountTypeNode`](../nodes/docs/typeNodes/AmountTypeNode.md)                           | `42`                                                        | Same as `NumberTypeNode`                                                                    |
+| [`ArrayTypeNode`](../nodes/docs/typeNodes/ArrayTypeNode.md)                             | `[1, 2, 3]`                                                 |                                                                                             |
+| [`BooleanTypeNode`](../nodes/docs/typeNodes/BooleanTypeNode.md)                         | `true` or `false`                                           |                                                                                             |
+| [`BytesTypeNode`](../nodes/docs/typeNodes/BytesTypeNode.md)                             | `["base16", "00ffaa"]`                                      | Uses `bytesEncoding` option to decode                                                       |
+| [`DateTimeTypeNode`](../nodes/docs/typeNodes/DateTimeTypeNode.md)                       | `42`                                                        | Same as `NumberTypeNode`                                                                    |
+| [`EnumTypeNode`](../nodes/docs/typeNodes/EnumTypeNode.md)                               | `2` or `{ __kind: "move", x: 12, y: 34 }`                   | Uses number indices for scalar enums. Uses discriminated unions otherwise.                  |
+| [`FixedSizeTypeNode`](../nodes/docs/typeNodes/FixedSizeTypeNode.md)                     | -                                                           | Same as `node.type`                                                                         |
+| [`HiddenPrefixTypeNode`](../nodes/docs/typeNodes/HiddenPrefixTypeNode.md)               | -                                                           | Same as `node.type`                                                                         |
+| [`HiddenSuffixTypeNode`](../nodes/docs/typeNodes/HiddenSuffixTypeNode.md)               | -                                                           | Same as `node.type`                                                                         |
+| [`MapTypeNode`](../nodes/docs/typeNodes/MapTypeNode.md)                                 | `{ key1: "value1", key2: "value2" }`                        | Represent `Maps` as `objects`                                                               |
+| [`NumberTypeNode`](../nodes/docs/typeNodes/NumberTypeNode.md)                           | `42`                                                        | This could be a `bigint`                                                                    |
+| [`OptionTypeNode`](../nodes/docs/typeNodes/OptionTypeNode.md)                           | `{ __option: "Some", value: 42 }` or `{ __option: "None" }` | Uses value objects (instead of `T \| null`) to avoid loosing information on nested options. |
+| [`PostOffsetTypeNode`](../nodes/docs/typeNodes/PostOffsetTypeNode.md)                   | -                                                           | Same as `node.type`                                                                         |
+| [`PreOffsetTypeNode`](../nodes/docs/typeNodes/PreOffsetTypeNode.md)                     | -                                                           | Same as `node.type`                                                                         |
+| [`PublicKeyTypeNode`](../nodes/docs/typeNodes/PublicKeyTypeNode.md)                     | `"3QC7Pnv2KfwwdC44gPcmQWuZXmRSbUpmWMJnhenMC8CU"`            | Uses base58 representations of public keys                                                  |
+| [`RemainderOptionTypeNode`](../nodes/docs/typeNodes/RemainderOptionTypeNode.md)         | `{ __option: "Some", value: 42 }` or `{ __option: "None" }` | Same as `OptionTypeNode`                                                                    |
+| [`SentinelTypeNode`](../nodes/docs/typeNodes/SentinelTypeNode.md)                       | -                                                           | Same as `node.type`                                                                         |
+| [`SetTypeNode`](../nodes/docs/typeNodes/SetTypeNode.md)                                 | `[1, 2, 3]`                                                 | Same as `ArrayTypeNode`                                                                     |
+| [`SizePrefixTypeNode`](../nodes/docs/typeNodes/SizePrefixTypeNode.md)                   | -                                                           | Same as `node.type`                                                                         |
+| [`SolAmountTypeNode`](../nodes/docs/typeNodes/SolAmountTypeNode.md)                     | `42`                                                        | Same as `NumberTypeNode`                                                                    |
+| [`StringTypeNode`](../nodes/docs/typeNodes/StringTypeNode.md)                           | `"Hello World"`                                             | Uses the encoding defined in the node — i.e. `node.encoding`                                |
+| [`StructTypeNode`](../nodes/docs/typeNodes/StructTypeNode.md)                           | `{ name: "John", age: 42 }`                                 |                                                                                             |
+| [`TupleTypeNode`](../nodes/docs/typeNodes/TupleTypeNode.md)                             | `["John", 42]`                                              | Uses arrays to create tuples                                                                |
+| [`ZeroableOptionTypeNode`](../nodes/docs/typeNodes/ZeroableOptionTypeNode.md)           | `{ __option: "Some", value: 42 }` or `{ __option: "None" }` | Same as `OptionTypeNode`                                                                    |
+
+### `getNodeCodecVisitor(linkables, options?)`
+
+This visitor is used by `getNodeCodec` under the hood. It returns a `Codec<unknown>` for the visited node.
+
+```ts
+return visit(someTypeNode, getNodeCodecVisitor(linkables));
+```
+
+### `getValueNodeVisitor(linkables, options?)`
+
+This visitor is used by the `getValueNodeVisitor` under the hood. It returns an `unknown` value for the visited `ValueNode`.
+
+```ts
+return visit(someValueNode, getValueNodeVisitor(linkables));
+```

+ 70 - 0
packages/dynamic-codecs/package.json

@@ -0,0 +1,70 @@
+{
+    "name": "@codama/dynamic-codecs",
+    "version": "1.0.0",
+    "description": "Get codecs on demand for Codama IDLs",
+    "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",
+        "codecs"
+    ],
+    "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/errors": "workspace:*",
+        "@codama/nodes": "workspace:*",
+        "@codama/visitors-core": "workspace:*",
+        "@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"
+    ]
+}

+ 419 - 0
packages/dynamic-codecs/src/codecs.ts

@@ -0,0 +1,419 @@
+import {
+    CODAMA_ERROR__UNRECOGNIZED_BYTES_ENCODING,
+    CODAMA_ERROR__UNRECOGNIZED_NUMBER_FORMAT,
+    CodamaError,
+} from '@codama/errors';
+import {
+    AccountLinkNode,
+    AccountNode,
+    BytesEncoding,
+    CountNode,
+    DefinedTypeLinkNode,
+    DefinedTypeNode,
+    InstructionArgumentLinkNode,
+    InstructionArgumentNode,
+    InstructionLinkNode,
+    InstructionNode,
+    isNode,
+    isScalarEnum,
+    NumberFormat,
+    pascalCase,
+    RegisteredTypeNode,
+    structFieldTypeNode,
+    structFieldTypeNodeFromInstructionArgumentNode,
+    structTypeNode,
+    structTypeNodeFromInstructionArgumentNodes,
+} from '@codama/nodes';
+import {
+    getLastNodeFromPath,
+    getRecordLinkablesVisitor,
+    LinkableDictionary,
+    NodePath,
+    NodeStack,
+    pipe,
+    recordNodeStackVisitor,
+    visit,
+    Visitor,
+} from '@codama/visitors-core';
+import {
+    addCodecSentinel,
+    addCodecSizePrefix,
+    assertIsFixedSize,
+    Codec,
+    createCodec,
+    fixCodecSize,
+    getArrayCodec,
+    getBase16Codec,
+    getBase58Codec,
+    getBase64Codec,
+    getBooleanCodec,
+    getConstantCodec,
+    getDiscriminatedUnionCodec,
+    getEnumCodec,
+    getF32Codec,
+    getF64Codec,
+    getHiddenPrefixCodec,
+    getHiddenSuffixCodec,
+    getI8Codec,
+    getI16Codec,
+    getI32Codec,
+    getI64Codec,
+    getI128Codec,
+    getMapCodec,
+    getOptionCodec,
+    getShortU16Codec,
+    getStructCodec,
+    getTupleCodec,
+    getU8Codec,
+    getU16Codec,
+    getU32Codec,
+    getU64Codec,
+    getU128Codec,
+    getUnitCodec,
+    getUtf8Codec,
+    NumberCodec,
+    offsetCodec,
+    padLeftCodec,
+    padRightCodec,
+    transformCodec,
+} from '@solana/codecs';
+
+import { getValueNodeVisitor } from './values';
+
+export type EncodableNodes =
+    | AccountLinkNode
+    | AccountNode
+    | DefinedTypeLinkNode
+    | DefinedTypeNode
+    | InstructionArgumentLinkNode
+    | InstructionArgumentNode
+    | InstructionLinkNode
+    | InstructionNode
+    | RegisteredTypeNode;
+
+export type CodecVisitorOptions = {
+    bytesEncoding?: BytesEncoding;
+};
+
+export function getNodeCodec(path: NodePath<EncodableNodes>, options: CodecVisitorOptions = {}): Codec<unknown> {
+    const linkables = new LinkableDictionary();
+    visit(path[0], getRecordLinkablesVisitor(linkables));
+
+    return visit(
+        getLastNodeFromPath(path),
+        getNodeCodecVisitor(linkables, {
+            stack: new NodeStack(path.slice(0, -1)),
+            ...options,
+        }),
+    );
+}
+
+export function getNodeCodecVisitor(
+    linkables: LinkableDictionary,
+    options: CodecVisitorOptions & { stack?: NodeStack } = {},
+): Visitor<Codec<unknown>, EncodableNodes['kind']> {
+    const stack = options.stack ?? new NodeStack();
+    const bytesEncoding = options.bytesEncoding ?? 'base64';
+    const valueNodeVisitor = getValueNodeVisitor(linkables, {
+        codecVisitorFactory: () => visitor,
+        stack,
+    });
+
+    const baseVisitor: Visitor<Codec<unknown>, EncodableNodes['kind']> = {
+        visitAccount(node) {
+            return visit(node.data, this);
+        },
+        visitAccountLink(node) {
+            const path = linkables.getPathOrThrow(stack.getPath(node.kind));
+            stack.pushPath(path);
+            const result = visit(getLastNodeFromPath(path), this);
+            stack.popPath();
+            return result;
+        },
+        visitAmountType(node) {
+            return visit(node.number, this);
+        },
+        visitArrayType(node) {
+            const item = visit(node.item, this);
+            const size = getSizeFromCountNode(node.count, this);
+            return getArrayCodec(item, { size }) as Codec<unknown>;
+        },
+        visitBooleanType(node) {
+            const size = visit(node.size, this) as NumberCodec;
+            return getBooleanCodec({ size }) as Codec<unknown>;
+        },
+        visitBytesType() {
+            // Note we use a format like `["base64", "someData"]` to encode bytes,
+            // instead of using `Uint8Arrays` in order to be compatible with JSON.
+            return createCodec<[BytesEncoding, string]>({
+                getSizeFromValue: ([encoding, value]) => {
+                    return getCodecFromBytesEncoding(encoding).getSizeFromValue(value);
+                },
+                read: (bytes, offset) => {
+                    const [value, newOffset] = getCodecFromBytesEncoding(bytesEncoding).read(bytes, offset);
+                    return [[bytesEncoding, value], newOffset];
+                },
+                write: ([encoding, value], bytes, offset) => {
+                    return getCodecFromBytesEncoding(encoding).write(value, bytes, offset);
+                },
+            }) as Codec<unknown>;
+        },
+        visitDateTimeType(node) {
+            return visit(node.number, this);
+        },
+        visitDefinedType(node) {
+            return visit(node.type, this);
+        },
+        visitDefinedTypeLink(node) {
+            const path = linkables.getPathOrThrow(stack.getPath(node.kind));
+            stack.pushPath(path);
+            const result = visit(getLastNodeFromPath(path), this);
+            stack.popPath();
+            return result;
+        },
+        visitEnumEmptyVariantType() {
+            return getUnitCodec() as Codec<unknown>;
+        },
+        visitEnumStructVariantType(node) {
+            return visit(node.struct, this);
+        },
+        visitEnumTupleVariantType(node) {
+            const tupleAsStruct = structTypeNode([structFieldTypeNode({ name: 'fields', type: node.tuple })]);
+            return visit(tupleAsStruct, this);
+        },
+        visitEnumType(node) {
+            const size = visit(node.size, this) as NumberCodec;
+            // Scalar enums are decoded as simple numbers.
+            if (isScalarEnum(node)) {
+                return getEnumCodec(
+                    Object.fromEntries(
+                        node.variants.flatMap((variant, index) => [
+                            [variant.name, index],
+                            [index, variant.name],
+                        ]),
+                    ),
+                    { size },
+                ) as Codec<unknown>;
+            }
+            // Data enums are decoded as discriminated unions, e.g. `{ __kind: 'Move', x: 10, y: 20 }`.
+            const variants = node.variants.map(variant => [pascalCase(variant.name), visit(variant, this)] as const);
+            return getDiscriminatedUnionCodec(variants, { size }) as unknown as Codec<unknown>;
+        },
+        visitFixedSizeType(node) {
+            const type = visit(node.type, this);
+            return fixCodecSize(type, node.size);
+        },
+        visitHiddenPrefixType(node) {
+            const type = visit(node.type, this);
+            const constants = node.prefix.map(constant => {
+                const constantCodec = visit(constant.type, this);
+                const constantValue = visit(constant.value, valueNodeVisitor);
+                return getConstantCodec(constantCodec.encode(constantValue));
+            });
+            return getHiddenPrefixCodec(type, constants);
+        },
+        visitHiddenSuffixType(node) {
+            const type = visit(node.type, this);
+            const constants = node.suffix.map(constant => {
+                const constantCodec = visit(constant.type, this);
+                const constantValue = visit(constant.value, valueNodeVisitor);
+                return getConstantCodec(constantCodec.encode(constantValue));
+            });
+            return getHiddenSuffixCodec(type, constants);
+        },
+        visitInstruction(node) {
+            return visit(structTypeNodeFromInstructionArgumentNodes(node.arguments), this);
+        },
+        visitInstructionArgument(node) {
+            return visit(structFieldTypeNodeFromInstructionArgumentNode(node), this);
+        },
+        visitInstructionArgumentLink(node) {
+            const path = linkables.getPathOrThrow(stack.getPath(node.kind));
+            stack.pushPath(path);
+            const result = visit(getLastNodeFromPath(path), this);
+            stack.popPath();
+            return result;
+        },
+        visitInstructionLink(node) {
+            const path = linkables.getPathOrThrow(stack.getPath(node.kind));
+            stack.pushPath(path);
+            const result = visit(getLastNodeFromPath(path), this);
+            stack.popPath();
+            return result;
+        },
+        visitMapType(node) {
+            const key = visit(node.key, this);
+            const value = visit(node.value, this);
+            const size = getSizeFromCountNode(node.count, this);
+            // Note we transform maps as objects to be compatible with JSON.
+            return transformCodec(
+                getMapCodec(key, value, { size }),
+                (value: object) => new Map(Object.entries(value)),
+                (map: Map<unknown, unknown>): object => Object.fromEntries(map),
+            ) as Codec<unknown>;
+        },
+        visitNumberType(node) {
+            return getCodecFromNumberFormat(node.format) as Codec<unknown>;
+        },
+        visitOptionType(node) {
+            const item = visit(node.item, this);
+            const prefix = visit(node.prefix, this) as NumberCodec;
+            if (node.fixed) {
+                assertIsFixedSize(item);
+                return getOptionCodec(item, { noneValue: 'zeroes', prefix });
+            }
+            return getOptionCodec(item, { prefix });
+        },
+        visitPostOffsetType(node) {
+            const type = visit(node.type, this);
+            switch (node.strategy) {
+                case 'padded':
+                    return padRightCodec(type, node.offset);
+                case 'absolute':
+                    return offsetCodec(type, {
+                        postOffset: ({ wrapBytes }) => (node.offset < 0 ? wrapBytes(node.offset) : node.offset),
+                    });
+                case 'preOffset':
+                    return offsetCodec(type, { postOffset: ({ preOffset }) => preOffset + node.offset });
+                case 'relative':
+                default:
+                    return offsetCodec(type, { postOffset: ({ postOffset }) => postOffset + node.offset });
+            }
+        },
+        visitPreOffsetType(node) {
+            const type = visit(node.type, this);
+            switch (node.strategy) {
+                case 'padded':
+                    return padLeftCodec(type, node.offset);
+                case 'absolute':
+                    return offsetCodec(type, {
+                        preOffset: ({ wrapBytes }) => (node.offset < 0 ? wrapBytes(node.offset) : node.offset),
+                    });
+                case 'relative':
+                default:
+                    return offsetCodec(type, { preOffset: ({ preOffset }) => preOffset + node.offset });
+            }
+        },
+        visitPublicKeyType() {
+            return fixCodecSize(getBase58Codec(), 32) as Codec<unknown>;
+        },
+        visitRemainderOptionType(node) {
+            const item = visit(node.item, this);
+            return getOptionCodec(item, { prefix: null });
+        },
+        visitSentinelType(node) {
+            const type = visit(node.type, this);
+            const sentinelCodec = visit(node.sentinel.type, this);
+            const sentinelValue = visit(node.sentinel.value, valueNodeVisitor);
+            const sentinelBytes = sentinelCodec.encode(sentinelValue);
+            return addCodecSentinel(type, sentinelBytes);
+        },
+        visitSetType(node) {
+            const item = visit(node.item, this);
+            const size = getSizeFromCountNode(node.count, this);
+            // Note we use the array codecs since it is compatible with the JSON format.
+            return getArrayCodec(item, { size }) as Codec<unknown>;
+        },
+        visitSizePrefixType(node) {
+            const type = visit(node.type, this);
+            const prefix = visit(node.prefix, this) as NumberCodec;
+            return addCodecSizePrefix(type, prefix);
+        },
+        visitSolAmountType(node) {
+            return visit(node.number, this);
+        },
+        visitStringType(node) {
+            return getCodecFromBytesEncoding(node.encoding) as Codec<unknown>;
+        },
+        visitStructFieldType(node) {
+            return visit(node.type, this);
+        },
+        visitStructType(node) {
+            const fields = node.fields.map(field => [field.name, visit(field, this)] as const);
+            return getStructCodec(fields) as Codec<unknown>;
+        },
+        visitTupleType(node) {
+            const items = node.items.map(item => visit(item, this));
+            return getTupleCodec(items) as Codec<unknown>;
+        },
+        visitZeroableOptionType(node) {
+            const item = visit(node.item, this);
+            assertIsFixedSize(item);
+            if (node.zeroValue) {
+                const noneCodec = visit(node.zeroValue.type, this);
+                const noneValue = visit(node.zeroValue.value, valueNodeVisitor);
+                const noneBytes = noneCodec.encode(noneValue);
+                return getOptionCodec(item, { noneValue: noneBytes, prefix: null });
+            }
+            return getOptionCodec(item, { noneValue: 'zeroes', prefix: null });
+        },
+    };
+
+    const visitor = pipe(baseVisitor, v => recordNodeStackVisitor(v, stack));
+    return visitor;
+}
+
+function getCodecFromBytesEncoding(encoding: BytesEncoding) {
+    switch (encoding) {
+        case 'base16':
+            return getBase16Codec();
+        case 'base58':
+            return getBase58Codec();
+        case 'base64':
+            return getBase64Codec();
+        case 'utf8':
+            return getUtf8Codec();
+        default:
+            throw new CodamaError(CODAMA_ERROR__UNRECOGNIZED_BYTES_ENCODING, {
+                encoding: encoding satisfies never,
+            });
+    }
+}
+
+function getCodecFromNumberFormat(format: NumberFormat) {
+    switch (format) {
+        case 'u8':
+            return getU8Codec();
+        case 'u16':
+            return getU16Codec();
+        case 'u32':
+            return getU32Codec();
+        case 'u64':
+            return getU64Codec();
+        case 'u128':
+            return getU128Codec();
+        case 'i8':
+            return getI8Codec();
+        case 'i16':
+            return getI16Codec();
+        case 'i32':
+            return getI32Codec();
+        case 'i64':
+            return getI64Codec();
+        case 'i128':
+            return getI128Codec();
+        case 'f32':
+            return getF32Codec();
+        case 'f64':
+            return getF64Codec();
+        case 'shortU16':
+            return getShortU16Codec();
+        default:
+            throw new CodamaError(CODAMA_ERROR__UNRECOGNIZED_NUMBER_FORMAT, {
+                format: format satisfies never,
+            });
+    }
+}
+
+function getSizeFromCountNode(
+    node: CountNode,
+    visitor: Visitor<unknown, EncodableNodes['kind']>,
+): NumberCodec | number | 'remainder' {
+    if (isNode(node, 'prefixedCountNode')) {
+        return visit(node.prefix, visitor) as NumberCodec;
+    }
+    if (isNode(node, 'fixedCountNode')) {
+        return node.value;
+    }
+    return 'remainder';
+}

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

@@ -0,0 +1,2 @@
+export * from './codecs';
+export * from './values';

+ 6 - 0
packages/dynamic-codecs/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;

+ 106 - 0
packages/dynamic-codecs/src/values.ts

@@ -0,0 +1,106 @@
+import { CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND, CodamaError } from '@codama/errors';
+import { assertIsNode, bytesTypeNode, isNode, isScalarEnum, pascalCase, ValueNode } from '@codama/nodes';
+import { LinkableDictionary, NodeStack, pipe, recordNodeStackVisitor, visit, Visitor } from '@codama/visitors-core';
+
+import { CodecVisitorOptions, getNodeCodecVisitor } from './codecs';
+
+export function getValueNodeVisitor(
+    linkables: LinkableDictionary,
+    options: {
+        codecVisitorFactory?: () => ReturnType<typeof getNodeCodecVisitor>;
+        codecVisitorOptions?: CodecVisitorOptions;
+        stack?: NodeStack;
+    } = {},
+): Visitor<unknown, ValueNode['kind']> {
+    const stack = options.stack ?? new NodeStack();
+    let cachedCodecVisitor: ReturnType<typeof getNodeCodecVisitor> | null = null;
+    const codecVisitorFactory =
+        options.codecVisitorFactory ??
+        (() => (cachedCodecVisitor ??= getNodeCodecVisitor(linkables, { stack, ...options.codecVisitorOptions })));
+
+    const baseVisitor: Visitor<unknown, ValueNode['kind']> = {
+        visitArrayValue(node) {
+            return node.items.map(item => visit(item, this));
+        },
+        visitBooleanValue(node) {
+            return node.boolean;
+        },
+        visitBytesValue(node) {
+            return [node.encoding, node.data];
+        },
+        visitConstantValue(node) {
+            const codec = visit(node.type, codecVisitorFactory());
+            const value = visit(node.value, this);
+            const bytes = codec.encode(value);
+            const bytesCodec = visit(bytesTypeNode(), codecVisitorFactory());
+            return bytesCodec.decode(bytes);
+        },
+        visitEnumValue(node) {
+            const enumType = linkables.getOrThrow([...stack.getPath(node.kind), node.enum]).type;
+            assertIsNode(enumType, 'enumTypeNode');
+            const variantIndex = enumType.variants.findIndex(variant => variant.name === node.variant);
+            if (variantIndex < 0) {
+                throw new CodamaError(CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND, {
+                    enum: node.enum,
+                    enumName: node.enum.name,
+                    variant: node.variant,
+                });
+            }
+            const variant = enumType.variants[variantIndex];
+            if (isScalarEnum(enumType)) return variantIndex;
+            const kind = { __kind: pascalCase(node.variant) };
+            if (isNode(variant, 'enumEmptyVariantTypeNode')) return kind;
+            if (isNode(variant, 'enumStructVariantTypeNode') && !!node.value) {
+                const value = visit(node.value, this) as object;
+                return { ...kind, ...value };
+            }
+            if (isNode(variant, 'enumTupleVariantTypeNode') && !!node.value) {
+                const fields = visit(node.value, this);
+                return { ...kind, fields };
+            }
+            return kind;
+        },
+        visitMapValue(node) {
+            return Object.fromEntries(
+                node.entries.map(entry => {
+                    const key = visit(entry.key, this);
+                    const value = visit(entry.value, this);
+                    return [key, value];
+                }),
+            );
+        },
+        visitNoneValue() {
+            return { __option: 'None' };
+        },
+        visitNumberValue(node) {
+            return node.number;
+        },
+        visitPublicKeyValue(node) {
+            return node.publicKey;
+        },
+        visitSetValue(node) {
+            return node.items.map(item => visit(item, this));
+        },
+        visitSomeValue(node) {
+            const value = visit(node.value, this);
+            return { __option: 'Some', value };
+        },
+        visitStringValue(node) {
+            return node.string;
+        },
+        visitStructValue(node) {
+            return Object.fromEntries(
+                node.fields.map(field => {
+                    const name = field.name;
+                    const value = visit(field.value, this);
+                    return [name, value];
+                }),
+            );
+        },
+        visitTupleValue(node) {
+            return node.items.map(item => visit(item, this));
+        },
+    };
+
+    return pipe(baseVisitor, v => recordNodeStackVisitor(v, stack));
+}

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

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

+ 21 - 0
packages/dynamic-codecs/test/codecs/AccountNode.test.ts

@@ -0,0 +1,21 @@
+import { accountNode, 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 data node', () => {
+    const codec = getNodeCodec([
+        accountNode({
+            data: structTypeNode([
+                structFieldTypeNode({
+                    name: 'foo',
+                    type: numberTypeNode('u32'),
+                }),
+            ]),
+            name: 'myAccount',
+        }),
+    ]);
+    expect(codec.encode({ foo: 42 })).toStrictEqual(hex('2a000000'));
+    expect(codec.decode(hex('2a000000'))).toStrictEqual({ foo: 42 });
+});

+ 23 - 0
packages/dynamic-codecs/test/codecs/ArrayTypeNode.test.ts

@@ -0,0 +1,23 @@
+import { arrayTypeNode, fixedCountNode, numberTypeNode, prefixedCountNode, remainderCountNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it decodes prefixed arrays', () => {
+    const codec = getNodeCodec([arrayTypeNode(numberTypeNode('u16'), prefixedCountNode(numberTypeNode('u32')))]);
+    expect(codec.encode([42, 99, 650])).toStrictEqual(hex('030000002a0063008a02'));
+    expect(codec.decode(hex('030000002a0063008a02'))).toStrictEqual([42, 99, 650]);
+});
+
+test('it decodes fixed arrays', () => {
+    const codec = getNodeCodec([arrayTypeNode(numberTypeNode('u16'), fixedCountNode(3))]);
+    expect(codec.encode([42, 99, 650])).toStrictEqual(hex('2a0063008a02'));
+    expect(codec.decode(hex('2a0063008a02'))).toStrictEqual([42, 99, 650]);
+});
+
+test('it decodes remainder arrays', () => {
+    const codec = getNodeCodec([arrayTypeNode(numberTypeNode('u16'), remainderCountNode())]);
+    expect(codec.encode([42, 99, 650])).toStrictEqual(hex('2a0063008a02'));
+    expect(codec.decode(hex('2a0063008a02'))).toStrictEqual([42, 99, 650]);
+});

+ 21 - 0
packages/dynamic-codecs/test/codecs/BooleanTypeNode.test.ts

@@ -0,0 +1,21 @@
+import { booleanTypeNode, numberTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('default', () => {
+    const codec = getNodeCodec([booleanTypeNode()]);
+    expect(codec.encode(true)).toStrictEqual(hex('01'));
+    expect(codec.decode(hex('01'))).toBe(true);
+    expect(codec.encode(false)).toStrictEqual(hex('00'));
+    expect(codec.decode(hex('00'))).toBe(false);
+});
+
+test('custom number', () => {
+    const codec = getNodeCodec([booleanTypeNode(numberTypeNode('u32'))]);
+    expect(codec.encode(true)).toStrictEqual(hex('01000000'));
+    expect(codec.decode(hex('01000000'))).toBe(true);
+    expect(codec.encode(false)).toStrictEqual(hex('00000000'));
+    expect(codec.decode(hex('00000000'))).toBe(false);
+});

+ 23 - 0
packages/dynamic-codecs/test/codecs/BytesTypeNode.test.ts

@@ -0,0 +1,23 @@
+import { bytesTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it uses base64 encoding by default', () => {
+    const codec = getNodeCodec([bytesTypeNode()]);
+    expect(codec.encode(['base64', 'HelloWorld++'])).toStrictEqual(hex('1de965a16a2b95dfbe'));
+    expect(codec.decode(hex('1de965a16a2b95dfbe'))).toStrictEqual(['base64', 'HelloWorld++']);
+});
+
+test('it can use a custom default encoding', () => {
+    const codec = getNodeCodec([bytesTypeNode()], { bytesEncoding: 'base16' });
+    expect(codec.encode(['base16', 'deadb0d1e5'])).toStrictEqual(hex('deadb0d1e5'));
+    expect(codec.decode(hex('deadb0d1e5'))).toStrictEqual(['base16', 'deadb0d1e5']);
+});
+
+test('the first tuple item is always used when encoding the data', () => {
+    const codec = getNodeCodec([bytesTypeNode()], { bytesEncoding: 'base64' });
+    expect(codec.encode(['base16', 'deadb0d1e5'])).toStrictEqual(hex('deadb0d1e5'));
+    expect(codec.decode(hex('deadb0d1e5'))).toStrictEqual(['base64', '3q2w0eU=']);
+});

+ 64 - 0
packages/dynamic-codecs/test/codecs/DefinedTypeLinkNode.test.ts

@@ -0,0 +1,64 @@
+import {
+    definedTypeLinkNode,
+    definedTypeNode,
+    numberTypeNode,
+    programLinkNode,
+    programNode,
+    rootNode,
+} from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it resolves the codec of defined type link nodes', () => {
+    // Given an existing defined type and a LinkNode pointing to it.
+    const root = rootNode(
+        programNode({
+            definedTypes: [
+                definedTypeNode({ name: 'slot', type: numberTypeNode('u64') }),
+                definedTypeNode({ name: 'lastSlot', type: definedTypeLinkNode('slot') }),
+            ],
+            name: 'myProgram',
+            publicKey: '1111',
+        }),
+    );
+
+    // When we get the codec for the defined type pointing to another defined type.
+    const codec = getNodeCodec([root, root.program, root.program.definedTypes[1]]);
+
+    // Then we expect the codec to match the linked defined type.
+    expect(codec.encode(42)).toStrictEqual(hex('2a00000000000000'));
+    expect(codec.decode(hex('2a00000000000000'))).toBe(42n);
+});
+
+test('it follows linked nodes using the correct paths', () => {
+    // Given two link nodes designed so that the path would
+    // fail if we did not save and restored linked paths.
+    const programA = programNode({
+        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('u64') }),
+        ],
+        name: 'programB',
+        publicKey: '2222',
+    });
+    const root = rootNode(programA, [programB]);
+
+    // When we get the codec for the defined type in programA.
+    const codec = getNodeCodec([root, programA, programA.definedTypes[0]]);
+
+    // Then we expect the links in programB to be resolved correctly.
+    expect(codec.encode(42)).toStrictEqual(hex('2a00000000000000'));
+    expect(codec.decode(hex('2a00000000000000'))).toBe(42n);
+});

+ 11 - 0
packages/dynamic-codecs/test/codecs/DefinedTypeNode.test.ts

@@ -0,0 +1,11 @@
+import { definedTypeNode, numberTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it delegates to the underlying type node', () => {
+    const codec = getNodeCodec([definedTypeNode({ name: 'foo', type: numberTypeNode('u32') })]);
+    expect(codec.encode(42)).toStrictEqual(hex('2a000000'));
+    expect(codec.decode(hex('2a000000'))).toBe(42);
+});

+ 79 - 0
packages/dynamic-codecs/test/codecs/EnumTypeNode.test.ts

@@ -0,0 +1,79 @@
+import {
+    enumEmptyVariantTypeNode,
+    enumStructVariantTypeNode,
+    enumTupleVariantTypeNode,
+    enumTypeNode,
+    fixedSizeTypeNode,
+    numberTypeNode,
+    stringTypeNode,
+    structFieldTypeNode,
+    structTypeNode,
+    tupleTypeNode,
+} from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it encodes scalar enums', () => {
+    const codec = getNodeCodec([enumTypeNode([enumEmptyVariantTypeNode('up'), enumEmptyVariantTypeNode('down')])]);
+    expect(codec.encode(0)).toStrictEqual(hex('00'));
+    expect(codec.decode(hex('00'))).toBe(0);
+    expect(codec.encode(1)).toStrictEqual(hex('01'));
+    expect(codec.decode(hex('01'))).toBe(1);
+});
+
+test('it encodes scalar enums with custom sizes', () => {
+    const codec = getNodeCodec([
+        enumTypeNode([enumEmptyVariantTypeNode('up'), enumEmptyVariantTypeNode('down')], {
+            size: numberTypeNode('u16'),
+        }),
+    ]);
+    expect(codec.encode(0)).toStrictEqual(hex('0000'));
+    expect(codec.decode(hex('0000'))).toBe(0);
+    expect(codec.encode(1)).toStrictEqual(hex('0100'));
+    expect(codec.decode(hex('0100'))).toBe(1);
+});
+
+test('it encodes data enums', () => {
+    const codec = getNodeCodec([
+        enumTypeNode([
+            enumEmptyVariantTypeNode('quit'),
+            enumTupleVariantTypeNode('write', tupleTypeNode([fixedSizeTypeNode(stringTypeNode('utf8'), 5)])),
+            enumStructVariantTypeNode(
+                'move',
+                structTypeNode([
+                    structFieldTypeNode({ name: 'x', type: numberTypeNode('u8') }),
+                    structFieldTypeNode({ name: 'y', type: numberTypeNode('u8') }),
+                ]),
+            ),
+        ]),
+    ]);
+    const quitVariant = { __kind: 'Quit' };
+    expect(codec.encode(quitVariant)).toStrictEqual(hex('00'));
+    expect(codec.decode(hex('00'))).toStrictEqual(quitVariant);
+    const writeVariant = { __kind: 'Write', fields: ['Hello'] };
+    expect(codec.encode(writeVariant)).toStrictEqual(hex('0148656c6c6f'));
+    expect(codec.decode(hex('0148656c6c6f'))).toStrictEqual(writeVariant);
+    const moveVariant = { __kind: 'Move', x: 10, y: 20 };
+    expect(codec.encode(moveVariant)).toStrictEqual(hex('020a14'));
+    expect(codec.decode(hex('020a14'))).toStrictEqual(moveVariant);
+});
+
+test('it encodes data enums with custom sizes', () => {
+    const codec = getNodeCodec([
+        enumTypeNode(
+            [
+                enumEmptyVariantTypeNode('quit'),
+                enumTupleVariantTypeNode('write', tupleTypeNode([fixedSizeTypeNode(stringTypeNode('utf8'), 5)])),
+            ],
+            { size: numberTypeNode('u16') },
+        ),
+    ]);
+    const quitVariant = { __kind: 'Quit' };
+    expect(codec.encode(quitVariant)).toStrictEqual(hex('0000'));
+    expect(codec.decode(hex('0000'))).toStrictEqual(quitVariant);
+    const writeVariant = { __kind: 'Write', fields: ['Hello'] };
+    expect(codec.encode(writeVariant)).toStrictEqual(hex('010048656c6c6f'));
+    expect(codec.decode(hex('010048656c6c6f'))).toStrictEqual(writeVariant);
+});

+ 13 - 0
packages/dynamic-codecs/test/codecs/FixedSizeTypeNode.test.ts

@@ -0,0 +1,13 @@
+import { fixedSizeTypeNode, stringTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it decodes fixed size strings', () => {
+    const codec = getNodeCodec([fixedSizeTypeNode(stringTypeNode('utf8'), 5)]);
+    expect(codec.encode('Hello')).toStrictEqual(hex('48656c6c6f'));
+    expect(codec.decode(hex('48656c6c6f'))).toBe('Hello');
+    expect(codec.encode('Sup')).toStrictEqual(hex('5375700000'));
+    expect(codec.decode(hex('5375700000'))).toBe('Sup');
+});

+ 22 - 0
packages/dynamic-codecs/test/codecs/HiddenPrefixTypeNode.test.ts

@@ -0,0 +1,22 @@
+import {
+    constantValueNode,
+    fixedSizeTypeNode,
+    hiddenPrefixTypeNode,
+    numberTypeNode,
+    numberValueNode,
+    stringTypeNode,
+} from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it hides hidden prefixes from the main type', () => {
+    const codec = getNodeCodec([
+        hiddenPrefixTypeNode(fixedSizeTypeNode(stringTypeNode('utf8'), 5), [
+            constantValueNode(numberTypeNode('u64'), numberValueNode(42)),
+        ]),
+    ]);
+    expect(codec.encode('Alice')).toStrictEqual(hex('2a00000000000000416c696365'));
+    expect(codec.decode(hex('2a00000000000000416c696365'))).toStrictEqual('Alice');
+});

+ 22 - 0
packages/dynamic-codecs/test/codecs/HiddenSuffixTypeNode.test.ts

@@ -0,0 +1,22 @@
+import {
+    constantValueNode,
+    fixedSizeTypeNode,
+    hiddenSuffixTypeNode,
+    numberTypeNode,
+    numberValueNode,
+    stringTypeNode,
+} from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it hides hidden suffixes from the main type', () => {
+    const codec = getNodeCodec([
+        hiddenSuffixTypeNode(fixedSizeTypeNode(stringTypeNode('utf8'), 5), [
+            constantValueNode(numberTypeNode('u64'), numberValueNode(42)),
+        ]),
+    ]);
+    expect(codec.encode('Alice')).toStrictEqual(hex('416c6963652a00000000000000'));
+    expect(codec.decode(hex('416c6963652a00000000000000'))).toStrictEqual('Alice');
+});

+ 16 - 0
packages/dynamic-codecs/test/codecs/InstructionArgumentNode.test.ts

@@ -0,0 +1,16 @@
+import { instructionArgumentNode, numberTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it delegates to the type node of the argument', () => {
+    const codec = getNodeCodec([
+        instructionArgumentNode({
+            name: 'foo',
+            type: numberTypeNode('u32'),
+        }),
+    ]);
+    expect(codec.encode(42)).toStrictEqual(hex('2a000000'));
+    expect(codec.decode(hex('2a000000'))).toStrictEqual(42);
+});

+ 21 - 0
packages/dynamic-codecs/test/codecs/InstructionNode.test.ts

@@ -0,0 +1,21 @@
+import { instructionArgumentNode, instructionNode, numberTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it delegates to the instruction arguments as a struct', () => {
+    const codec = getNodeCodec([
+        instructionNode({
+            arguments: [
+                instructionArgumentNode({
+                    name: 'foo',
+                    type: numberTypeNode('u32'),
+                }),
+            ],
+            name: 'myInstruction',
+        }),
+    ]);
+    expect(codec.encode({ foo: 42 })).toStrictEqual(hex('2a000000'));
+    expect(codec.decode(hex('2a000000'))).toStrictEqual({ foo: 42 });
+});

+ 43 - 0
packages/dynamic-codecs/test/codecs/MapTypeNode.test.ts

@@ -0,0 +1,43 @@
+import {
+    fixedCountNode,
+    fixedSizeTypeNode,
+    mapTypeNode,
+    numberTypeNode,
+    prefixedCountNode,
+    remainderCountNode,
+    stringTypeNode,
+} from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it decodes prefixed maps as objects', () => {
+    const key = fixedSizeTypeNode(stringTypeNode('utf8'), 3);
+    const value = numberTypeNode('u16');
+    const codec = getNodeCodec([mapTypeNode(key, value, prefixedCountNode(numberTypeNode('u32')))]);
+    // eslint-disable-next-line sort-keys-fix/sort-keys-fix
+    const map = { foo: 42, bar: 99, baz: 650 };
+    expect(codec.encode(map)).toStrictEqual(hex('03000000666f6f2a00626172630062617a8a02'));
+    expect(codec.decode(hex('03000000666f6f2a00626172630062617a8a02'))).toStrictEqual(map);
+});
+
+test('it decodes fixed maps as objects', () => {
+    const key = fixedSizeTypeNode(stringTypeNode('utf8'), 3);
+    const value = numberTypeNode('u16');
+    const codec = getNodeCodec([mapTypeNode(key, value, fixedCountNode(3))]);
+    // eslint-disable-next-line sort-keys-fix/sort-keys-fix
+    const map = { foo: 42, bar: 99, baz: 650 };
+    expect(codec.encode(map)).toStrictEqual(hex('666f6f2a00626172630062617a8a02'));
+    expect(codec.decode(hex('666f6f2a00626172630062617a8a02'))).toStrictEqual(map);
+});
+
+test('it decodes remainder maps as objects', () => {
+    const key = fixedSizeTypeNode(stringTypeNode('utf8'), 3);
+    const value = numberTypeNode('u16');
+    const codec = getNodeCodec([mapTypeNode(key, value, remainderCountNode())]);
+    // eslint-disable-next-line sort-keys-fix/sort-keys-fix
+    const map = { foo: 42, bar: 99, baz: 650 };
+    expect(codec.encode(map)).toStrictEqual(hex('666f6f2a00626172630062617a8a02'));
+    expect(codec.decode(hex('666f6f2a00626172630062617a8a02'))).toStrictEqual(map);
+});

+ 85 - 0
packages/dynamic-codecs/test/codecs/NumberTypeNode.test.ts

@@ -0,0 +1,85 @@
+import { numberTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('u8', () => {
+    const codec = getNodeCodec([numberTypeNode('u8')]);
+    expect(codec.encode(42)).toStrictEqual(hex('2a'));
+    expect(codec.decode(hex('2a'))).toBe(42);
+});
+
+test('u16', () => {
+    const codec = getNodeCodec([numberTypeNode('u16')]);
+    expect(codec.encode(42)).toStrictEqual(hex('2a00'));
+    expect(codec.decode(hex('2a00'))).toBe(42);
+});
+
+test('u32', () => {
+    const codec = getNodeCodec([numberTypeNode('u32')]);
+    expect(codec.encode(42)).toStrictEqual(hex('2a000000'));
+    expect(codec.decode(hex('2a000000'))).toBe(42);
+});
+
+test('u64', () => {
+    const codec = getNodeCodec([numberTypeNode('u64')]);
+    expect(codec.encode(42)).toStrictEqual(hex('2a00000000000000'));
+    expect(codec.decode(hex('2a00000000000000'))).toBe(42n);
+});
+
+test('u128', () => {
+    const codec = getNodeCodec([numberTypeNode('u128')]);
+    expect(codec.encode(42)).toStrictEqual(hex('2a000000000000000000000000000000'));
+    expect(codec.decode(hex('2a000000000000000000000000000000'))).toBe(42n);
+});
+
+test('i8', () => {
+    const codec = getNodeCodec([numberTypeNode('i8')]);
+    expect(codec.encode(-42)).toStrictEqual(hex('d6'));
+    expect(codec.decode(hex('d6'))).toBe(-42);
+});
+
+test('i16', () => {
+    const codec = getNodeCodec([numberTypeNode('i16')]);
+    expect(codec.encode(-42)).toStrictEqual(hex('d6ff'));
+    expect(codec.decode(hex('d6ff'))).toBe(-42);
+});
+
+test('i32', () => {
+    const codec = getNodeCodec([numberTypeNode('i32')]);
+    expect(codec.encode(-42)).toStrictEqual(hex('d6ffffff'));
+    expect(codec.decode(hex('d6ffffff'))).toBe(-42);
+});
+
+test('i64', () => {
+    const codec = getNodeCodec([numberTypeNode('i64')]);
+    expect(codec.encode(-42)).toStrictEqual(hex('d6ffffffffffffff'));
+    expect(codec.decode(hex('d6ffffffffffffff'))).toBe(-42n);
+});
+
+test('i128', () => {
+    const codec = getNodeCodec([numberTypeNode('i128')]);
+    expect(codec.encode(-42)).toStrictEqual(hex('d6ffffffffffffffffffffffffffffff'));
+    expect(codec.decode(hex('d6ffffffffffffffffffffffffffffff'))).toBe(-42n);
+});
+
+test('f32', () => {
+    const codec = getNodeCodec([numberTypeNode('f32')]);
+    expect(codec.encode(1.5)).toStrictEqual(hex('0000c03f'));
+    expect(codec.decode(hex('0000c03f'))).toBe(1.5);
+});
+
+test('f64', () => {
+    const codec = getNodeCodec([numberTypeNode('f64')]);
+    expect(codec.encode(1.5)).toStrictEqual(hex('000000000000f83f'));
+    expect(codec.decode(hex('000000000000f83f'))).toBe(1.5);
+});
+
+test('shortU16', () => {
+    const codec = getNodeCodec([numberTypeNode('shortU16')]);
+    expect(codec.encode(42)).toStrictEqual(hex('2a'));
+    expect(codec.decode(hex('2a'))).toBe(42);
+    expect(codec.encode(128)).toStrictEqual(hex('8001'));
+    expect(codec.decode(hex('8001'))).toBe(128);
+});

+ 29 - 0
packages/dynamic-codecs/test/codecs/OptionTypeNode.test.ts

@@ -0,0 +1,29 @@
+import { numberTypeNode, optionTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it encodes prefixed options', () => {
+    const codec = getNodeCodec([optionTypeNode(numberTypeNode('u16'))]);
+    expect(codec.encode({ __option: 'Some', value: 42 })).toStrictEqual(hex('012a00'));
+    expect(codec.decode(hex('012a00'))).toStrictEqual({ __option: 'Some', value: 42 });
+    expect(codec.encode({ __option: 'None' })).toStrictEqual(hex('00'));
+    expect(codec.decode(hex('00'))).toStrictEqual({ __option: 'None' });
+});
+
+test('it encodes prefixed options with custom sizes', () => {
+    const codec = getNodeCodec([optionTypeNode(numberTypeNode('u16'), { prefix: numberTypeNode('u32') })]);
+    expect(codec.encode({ __option: 'Some', value: 42 })).toStrictEqual(hex('010000002a00'));
+    expect(codec.decode(hex('010000002a00'))).toStrictEqual({ __option: 'Some', value: 42 });
+    expect(codec.encode({ __option: 'None' })).toStrictEqual(hex('00000000'));
+    expect(codec.decode(hex('00000000'))).toStrictEqual({ __option: 'None' });
+});
+
+test('it encodes prefixed options with fixed size items', () => {
+    const codec = getNodeCodec([optionTypeNode(numberTypeNode('u16'), { fixed: true })]);
+    expect(codec.encode({ __option: 'Some', value: 42 })).toStrictEqual(hex('012a00'));
+    expect(codec.decode(hex('012a00'))).toStrictEqual({ __option: 'Some', value: 42 });
+    expect(codec.encode({ __option: 'None' })).toStrictEqual(hex('000000'));
+    expect(codec.decode(hex('000000'))).toStrictEqual({ __option: 'None' });
+});

+ 42 - 0
packages/dynamic-codecs/test/codecs/PostOffsetTypeNode.test.ts

@@ -0,0 +1,42 @@
+import { fixedSizeTypeNode, numberTypeNode, postOffsetTypeNode, preOffsetTypeNode, tupleTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it encodes relative post-offsets', () => {
+    const node = tupleTypeNode([
+        postOffsetTypeNode(fixedSizeTypeNode(numberTypeNode('u8'), 4), -2),
+        numberTypeNode('u8'),
+    ]);
+    const codec = getNodeCodec([node]);
+    expect(codec.encode([0xaa, 0xff])).toStrictEqual(hex('aa00ff0000'));
+    expect(codec.decode(hex('aa00ff0000'))).toStrictEqual([0xaa, 0xff]);
+});
+
+test('it encodes padded post-offsets', () => {
+    const node = tupleTypeNode([postOffsetTypeNode(numberTypeNode('u8'), 4, 'padded'), numberTypeNode('u8')]);
+    const codec = getNodeCodec([node]);
+    expect(codec.encode([0xaa, 0xff])).toStrictEqual(hex('aa00000000ff'));
+    expect(codec.decode(hex('aa00000000ff'))).toStrictEqual([0xaa, 0xff]);
+});
+
+test('it encodes absolute post-offsets', () => {
+    const node = tupleTypeNode([
+        postOffsetTypeNode(fixedSizeTypeNode(numberTypeNode('u8'), 4), -2, 'absolute'),
+        numberTypeNode('u8'),
+    ]);
+    const codec = getNodeCodec([node]);
+    expect(codec.encode([0xaa, 0xff])).toStrictEqual(hex('aa0000ff00'));
+    expect(codec.decode(hex('aa0000ff00'))).toStrictEqual([0xaa, 0xff]);
+});
+
+test('it encodes post-offsets relative to the previous pre-offset', () => {
+    const node = tupleTypeNode([
+        postOffsetTypeNode(preOffsetTypeNode(numberTypeNode('u8'), 4, 'padded'), 0, 'preOffset'),
+        numberTypeNode('u8'),
+    ]);
+    const codec = getNodeCodec([node]);
+    expect(codec.encode([0xaa, 0xff])).toStrictEqual(hex('ff000000aa00'));
+    expect(codec.decode(hex('ff000000aa00'))).toStrictEqual([0xaa, 0xff]);
+});

+ 32 - 0
packages/dynamic-codecs/test/codecs/PreOffsetTypeNode.test.ts

@@ -0,0 +1,32 @@
+import { numberTypeNode, preOffsetTypeNode, tupleTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it encodes relative pre-offsets', () => {
+    const node = tupleTypeNode([
+        preOffsetTypeNode(numberTypeNode('u8'), 1),
+        preOffsetTypeNode(numberTypeNode('u8'), -2),
+    ]);
+    const codec = getNodeCodec([node]);
+    expect(codec.encode([0xaa, 0xff])).toStrictEqual(hex('ffaa'));
+    expect(codec.decode(hex('ffaa'))).toStrictEqual([0xaa, 0xff]);
+});
+
+test('it encodes padded pre-offsets', () => {
+    const node = tupleTypeNode([preOffsetTypeNode(numberTypeNode('u8'), 4, 'padded'), numberTypeNode('u8')]);
+    const codec = getNodeCodec([node]);
+    expect(codec.encode([0xaa, 0xff])).toStrictEqual(hex('00000000aaff'));
+    expect(codec.decode(hex('00000000aaff'))).toStrictEqual([0xaa, 0xff]);
+});
+
+test('it encodes absolute pre-offsets', () => {
+    const node = tupleTypeNode([
+        preOffsetTypeNode(numberTypeNode('u8'), 1),
+        preOffsetTypeNode(numberTypeNode('u8'), 0, 'absolute'),
+    ]);
+    const codec = getNodeCodec([node]);
+    expect(codec.encode([0xaa, 0xff])).toStrictEqual(hex('ffaa'));
+    expect(codec.decode(hex('ffaa'))).toStrictEqual([0xaa, 0xff]);
+});

+ 15 - 0
packages/dynamic-codecs/test/codecs/PublicKeyTypeNode.test.ts

@@ -0,0 +1,15 @@
+import { publicKeyTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it decodes as a base58 string', () => {
+    const codec = getNodeCodec([publicKeyTypeNode()]);
+    expect(codec.encode('LorisCg1FTs89a32VSrFskYDgiRbNQzct1WxyZb7nuA')).toStrictEqual(
+        hex('0513045e052f4919b608963de73c666e0672e06e28140ab841bff1cc83a178b5'),
+    );
+    expect(codec.decode(hex('0513045e052f4919b608963de73c666e0672e06e28140ab841bff1cc83a178b5'))).toBe(
+        'LorisCg1FTs89a32VSrFskYDgiRbNQzct1WxyZb7nuA',
+    );
+});

+ 13 - 0
packages/dynamic-codecs/test/codecs/RemainderOptionTypeNode.test.ts

@@ -0,0 +1,13 @@
+import { numberTypeNode, remainderOptionTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it encodes remainder options', () => {
+    const codec = getNodeCodec([remainderOptionTypeNode(numberTypeNode('u16'))]);
+    expect(codec.encode({ __option: 'Some', value: 42 })).toStrictEqual(hex('2a00'));
+    expect(codec.decode(hex('2a00'))).toStrictEqual({ __option: 'Some', value: 42 });
+    expect(codec.encode({ __option: 'None' })).toStrictEqual(hex(''));
+    expect(codec.decode(hex(''))).toStrictEqual({ __option: 'None' });
+});

+ 12 - 0
packages/dynamic-codecs/test/codecs/SentinelTypeNode.test.ts

@@ -0,0 +1,12 @@
+import { constantValueNodeFromBytes, sentinelTypeNode, stringTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it encodes sentinel types', () => {
+    const sentinel = constantValueNodeFromBytes('base16', 'ffff');
+    const codec = getNodeCodec([sentinelTypeNode(stringTypeNode('utf8'), sentinel)]);
+    expect(codec.encode('Hello World!')).toStrictEqual(hex('48656c6c6f20576f726c6421ffff'));
+    expect(codec.decode(hex('48656c6c6f20576f726c6421ffff'))).toBe('Hello World!');
+});

+ 23 - 0
packages/dynamic-codecs/test/codecs/SetTypeNode.test.ts

@@ -0,0 +1,23 @@
+import { fixedCountNode, numberTypeNode, prefixedCountNode, remainderCountNode, setTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it decodes prefixed sets', () => {
+    const codec = getNodeCodec([setTypeNode(numberTypeNode('u16'), prefixedCountNode(numberTypeNode('u32')))]);
+    expect(codec.encode([42, 99, 650])).toStrictEqual(hex('030000002a0063008a02'));
+    expect(codec.decode(hex('030000002a0063008a02'))).toStrictEqual([42, 99, 650]);
+});
+
+test('it decodes fixed sets', () => {
+    const codec = getNodeCodec([setTypeNode(numberTypeNode('u16'), fixedCountNode(3))]);
+    expect(codec.encode([42, 99, 650])).toStrictEqual(hex('2a0063008a02'));
+    expect(codec.decode(hex('2a0063008a02'))).toStrictEqual([42, 99, 650]);
+});
+
+test('it decodes remainder sets', () => {
+    const codec = getNodeCodec([setTypeNode(numberTypeNode('u16'), remainderCountNode())]);
+    expect(codec.encode([42, 99, 650])).toStrictEqual(hex('2a0063008a02'));
+    expect(codec.decode(hex('2a0063008a02'))).toStrictEqual([42, 99, 650]);
+});

+ 11 - 0
packages/dynamic-codecs/test/codecs/SizePrefixTypeNode.test.ts

@@ -0,0 +1,11 @@
+import { numberTypeNode, sizePrefixTypeNode, stringTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it encodes types prefixed with their sizes', () => {
+    const codec = getNodeCodec([sizePrefixTypeNode(stringTypeNode('utf8'), numberTypeNode('u32'))]);
+    expect(codec.encode('Hello World!')).toStrictEqual(hex('0c00000048656c6c6f20576f726c6421'));
+    expect(codec.decode(hex('0c00000048656c6c6f20576f726c6421'))).toBe('Hello World!');
+});

+ 29 - 0
packages/dynamic-codecs/test/codecs/StringTypeNode.test.ts

@@ -0,0 +1,29 @@
+import { stringTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('base16', () => {
+    const codec = getNodeCodec([stringTypeNode('base16')]);
+    expect(codec.encode('deadb0d1e5')).toStrictEqual(hex('deadb0d1e5'));
+    expect(codec.decode(hex('deadb0d1e5'))).toBe('deadb0d1e5');
+});
+
+test('base58', () => {
+    const codec = getNodeCodec([stringTypeNode('base58')]);
+    expect(codec.encode('heLLo')).toStrictEqual(hex('1b6a3070'));
+    expect(codec.decode(hex('1b6a3070'))).toBe('heLLo');
+});
+
+test('base64', () => {
+    const codec = getNodeCodec([stringTypeNode('base64')]);
+    expect(codec.encode('HelloWorld++')).toStrictEqual(hex('1de965a16a2b95dfbe'));
+    expect(codec.decode(hex('1de965a16a2b95dfbe'))).toBe('HelloWorld++');
+});
+
+test('utf8', () => {
+    const codec = getNodeCodec([stringTypeNode('utf8')]);
+    expect(codec.encode('Hello World!')).toStrictEqual(hex('48656c6c6f20576f726c6421'));
+    expect(codec.decode(hex('48656c6c6f20576f726c6421'))).toBe('Hello World!');
+});

+ 11 - 0
packages/dynamic-codecs/test/codecs/StructFieldTypeNode.test.ts

@@ -0,0 +1,11 @@
+import { numberTypeNode, structFieldTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it encodes struct fields using their types', () => {
+    const codec = getNodeCodec([structFieldTypeNode({ name: 'age', type: numberTypeNode('u16') })]);
+    expect(codec.encode(42)).toStrictEqual(hex('2a00'));
+    expect(codec.decode(hex('2a00'))).toStrictEqual(42);
+});

+ 17 - 0
packages/dynamic-codecs/test/codecs/StructTypeNode.test.ts

@@ -0,0 +1,17 @@
+import { fixedSizeTypeNode, numberTypeNode, stringTypeNode, structFieldTypeNode, structTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it encodes structs', () => {
+    const codec = getNodeCodec([
+        structTypeNode([
+            structFieldTypeNode({ name: 'firstname', type: fixedSizeTypeNode(stringTypeNode('utf8'), 5) }),
+            structFieldTypeNode({ name: 'age', type: numberTypeNode('u16') }),
+        ]),
+    ]);
+    const person = { age: 42, firstname: 'Alice' };
+    expect(codec.encode(person)).toStrictEqual(hex('416c6963652a00'));
+    expect(codec.decode(hex('416c6963652a00'))).toStrictEqual(person);
+});

+ 11 - 0
packages/dynamic-codecs/test/codecs/TupleTypeNode.test.ts

@@ -0,0 +1,11 @@
+import { fixedSizeTypeNode, numberTypeNode, stringTypeNode, tupleTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it encodes tuples', () => {
+    const codec = getNodeCodec([tupleTypeNode([fixedSizeTypeNode(stringTypeNode('utf8'), 3), numberTypeNode('u16')])]);
+    expect(codec.encode(['foo', 42])).toStrictEqual(hex('666f6f2a00'));
+    expect(codec.decode(hex('666f6f2a00'))).toStrictEqual(['foo', 42]);
+});

+ 22 - 0
packages/dynamic-codecs/test/codecs/ZeroableOptionTypeNode.test.ts

@@ -0,0 +1,22 @@
+import { constantValueNodeFromBytes, numberTypeNode, zeroableOptionTypeNode } from '@codama/nodes';
+import { expect, test } from 'vitest';
+
+import { getNodeCodec } from '../../src';
+import { hex } from '../_setup';
+
+test('it encodes zeroable options', () => {
+    const codec = getNodeCodec([zeroableOptionTypeNode(numberTypeNode('u16'))]);
+    expect(codec.encode({ __option: 'Some', value: 42 })).toStrictEqual(hex('2a00'));
+    expect(codec.decode(hex('2a00'))).toStrictEqual({ __option: 'Some', value: 42 });
+    expect(codec.encode({ __option: 'None' })).toStrictEqual(hex('0000'));
+    expect(codec.decode(hex('0000'))).toStrictEqual({ __option: 'None' });
+});
+
+test('it encodes zeroable options with custom zero values', () => {
+    const zeroValue = constantValueNodeFromBytes('base16', 'ffff');
+    const codec = getNodeCodec([zeroableOptionTypeNode(numberTypeNode('u16'), zeroValue)]);
+    expect(codec.encode({ __option: 'Some', value: 42 })).toStrictEqual(hex('2a00'));
+    expect(codec.decode(hex('2a00'))).toStrictEqual({ __option: 'Some', value: 42 });
+    expect(codec.encode({ __option: 'None' })).toStrictEqual(hex('ffff'));
+    expect(codec.decode(hex('ffff'))).toStrictEqual({ __option: 'None' });
+});

+ 6 - 0
packages/dynamic-codecs/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;

+ 11 - 0
packages/dynamic-codecs/test/values/ArrayValueNode.test.ts

@@ -0,0 +1,11 @@
+import { arrayValueNode, numberValueNode } from '@codama/nodes';
+import { LinkableDictionary, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getValueNodeVisitor } from '../../src';
+
+test('it returns an array of all resolved value nodes', () => {
+    const node = arrayValueNode([numberValueNode(1), numberValueNode(2), numberValueNode(3)]);
+    const result = visit(node, getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toStrictEqual([1, 2, 3]);
+});

+ 11 - 0
packages/dynamic-codecs/test/values/BytesValueNode.test.ts

@@ -0,0 +1,11 @@
+import { bytesValueNode } from '@codama/nodes';
+import { LinkableDictionary, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getValueNodeVisitor } from '../../src';
+
+test('it returns a tuple with encoding and encoded data', () => {
+    const node = bytesValueNode('base58', 'heLLo');
+    const result = visit(node, getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toStrictEqual(['base58', 'heLLo']);
+});

+ 91 - 0
packages/dynamic-codecs/test/values/ConstantValueNode.test.ts

@@ -0,0 +1,91 @@
+import {
+    booleanTypeNode,
+    booleanValueNode,
+    bytesTypeNode,
+    bytesValueNode,
+    constantValueNode,
+    fixedSizeTypeNode,
+    noneValueNode,
+    numberTypeNode,
+    numberValueNode,
+    optionTypeNode,
+    someValueNode,
+    stringTypeNode,
+    stringValueNode,
+    structFieldTypeNode,
+    structFieldValueNode,
+    structTypeNode,
+    structValueNode,
+} from '@codama/nodes';
+import { LinkableDictionary, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getNodeCodecVisitor, getValueNodeVisitor } from '../../src';
+
+test('it returns bytes from encoded numbers', () => {
+    const node = constantValueNode(numberTypeNode('u32'), numberValueNode(42));
+    const result = visit(node, getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toStrictEqual(['base64', 'KgAAAA==']);
+});
+
+test('it uses the default byte encoding from the codec visitor', () => {
+    const node = constantValueNode(numberTypeNode('u32'), numberValueNode(42));
+    const linkables = new LinkableDictionary();
+    const codecVisitorFactory = () => getNodeCodecVisitor(linkables, { bytesEncoding: 'base16' });
+    const result = visit(node, getValueNodeVisitor(linkables, { codecVisitorFactory }));
+    expect(result).toStrictEqual(['base16', '2a000000']);
+});
+
+test('it uses the default byte encoding from the codec visitor options', () => {
+    const node = constantValueNode(numberTypeNode('u32'), numberValueNode(42));
+    const result = visit(
+        node,
+        getValueNodeVisitor(new LinkableDictionary(), { codecVisitorOptions: { bytesEncoding: 'base16' } }),
+    );
+    expect(result).toStrictEqual(['base16', '2a000000']);
+});
+
+test('it returns bytes from byte values', () => {
+    const node = constantValueNode(bytesTypeNode(), bytesValueNode('base16', 'deadb0d1e5'));
+    const result = visit(node, getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toStrictEqual(['base64', '3q2w0eU=']);
+});
+
+test('it returns bytes from string values', () => {
+    const node = constantValueNode(stringTypeNode('base16'), stringValueNode('deadb0d1e5'));
+    const result = visit(node, getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toStrictEqual(['base64', '3q2w0eU=']);
+});
+
+test('it returns bytes from boolean values', () => {
+    const visitor = getValueNodeVisitor(new LinkableDictionary(), { codecVisitorOptions: { bytesEncoding: 'base16' } });
+    const resultFalse = visit(constantValueNode(booleanTypeNode(), booleanValueNode(false)), visitor);
+    const resultTrue = visit(constantValueNode(booleanTypeNode(), booleanValueNode(true)), visitor);
+    expect(resultFalse).toStrictEqual(['base16', '00']);
+    expect(resultTrue).toStrictEqual(['base16', '01']);
+});
+
+test('it returns bytes from struct values', () => {
+    const node = constantValueNode(
+        structTypeNode([
+            structFieldTypeNode({ name: 'firstname', type: fixedSizeTypeNode(stringTypeNode('utf8'), 5) }),
+            structFieldTypeNode({ name: 'age', type: numberTypeNode('u16') }),
+        ]),
+        structValueNode([
+            structFieldValueNode('firstname', stringValueNode('John')),
+            structFieldValueNode('age', stringValueNode('42')),
+        ]),
+    );
+    const visitor = getValueNodeVisitor(new LinkableDictionary(), { codecVisitorOptions: { bytesEncoding: 'base16' } });
+    const result = visit(node, visitor);
+    expect(result).toStrictEqual(['base16', '4a6f686e002a00']);
+});
+
+test('it returns bytes from option values', () => {
+    const visitor = getValueNodeVisitor(new LinkableDictionary(), { codecVisitorOptions: { bytesEncoding: 'base16' } });
+    const type = optionTypeNode(numberTypeNode('u16'));
+    const resultNone = visit(constantValueNode(type, noneValueNode()), visitor);
+    const resultSome = visit(constantValueNode(type, someValueNode(numberValueNode(42))), visitor);
+    expect(resultNone).toStrictEqual(['base16', '00']);
+    expect(resultSome).toStrictEqual(['base16', '012a00']);
+});

+ 106 - 0
packages/dynamic-codecs/test/values/EnumValueNode.test.ts

@@ -0,0 +1,106 @@
+import {
+    definedTypeNode,
+    enumEmptyVariantTypeNode,
+    enumStructVariantTypeNode,
+    enumTupleVariantTypeNode,
+    enumTypeNode,
+    enumValueNode,
+    fixedSizeTypeNode,
+    numberTypeNode,
+    numberValueNode,
+    programNode,
+    rootNode,
+    stringTypeNode,
+    stringValueNode,
+    structFieldTypeNode,
+    structFieldValueNode,
+    structTypeNode,
+    structValueNode,
+    tupleTypeNode,
+    tupleValueNode,
+} from '@codama/nodes';
+import { LinkableDictionary, NodeStack, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getValueNodeVisitor } from '../../src';
+
+test('it returns scalar enum values as numbers', () => {
+    // Given a program with a scalar enum.
+    const definedType = definedTypeNode({
+        name: 'direction',
+        type: enumTypeNode([
+            enumEmptyVariantTypeNode('up'),
+            enumEmptyVariantTypeNode('right'),
+            enumEmptyVariantTypeNode('down'),
+            enumEmptyVariantTypeNode('left'),
+        ]),
+    });
+    const root = rootNode(programNode({ definedTypes: [definedType], name: 'myProgram', publicKey: '1111' }));
+
+    // And a LinkableDictionary that recorded the enum.
+    const linkables = new LinkableDictionary();
+    linkables.recordPath([root, root.program, definedType]);
+
+    // And a value node visitor that's under the same program.
+    const stack = new NodeStack([root, root.program]);
+    const visitor = getValueNodeVisitor(linkables, { stack });
+
+    // When we visit enum value nodes for this enum type.
+    const resultUp = visit(enumValueNode('direction', 'up'), visitor);
+    const resultRight = visit(enumValueNode('direction', 'right'), visitor);
+    const resultDown = visit(enumValueNode('direction', 'down'), visitor);
+    const resultLeft = visit(enumValueNode('direction', 'left'), visitor);
+
+    // Then we expect the values to be resolved from the linkable type as numbers.
+    expect(resultUp).toBe(0);
+    expect(resultRight).toBe(1);
+    expect(resultDown).toBe(2);
+    expect(resultLeft).toBe(3);
+});
+
+test('it returns data enum values as objects', () => {
+    // Given a program with a data enum.
+    const definedType = definedTypeNode({
+        name: 'action',
+        type: enumTypeNode([
+            enumEmptyVariantTypeNode('quit'),
+            enumTupleVariantTypeNode('write', tupleTypeNode([fixedSizeTypeNode(stringTypeNode('utf8'), 5)])),
+            enumStructVariantTypeNode(
+                'move',
+                structTypeNode([
+                    structFieldTypeNode({ name: 'x', type: numberTypeNode('u8') }),
+                    structFieldTypeNode({ name: 'y', type: numberTypeNode('u8') }),
+                ]),
+            ),
+        ]),
+    });
+    const root = rootNode(programNode({ definedTypes: [definedType], name: 'myProgram', publicKey: '1111' }));
+
+    // And a LinkableDictionary that recorded the enum.
+    const linkables = new LinkableDictionary();
+    linkables.recordPath([root, root.program, definedType]);
+
+    // And a value node visitor that's under the same program.
+    const stack = new NodeStack([root, root.program]);
+    const visitor = getValueNodeVisitor(linkables, { stack });
+
+    // When we visit enum value nodes for this enum type.
+    const resultQuit = visit(enumValueNode('action', 'quit'), visitor);
+    const resultWrite = visit(enumValueNode('action', 'write', tupleValueNode([stringValueNode('Hello')])), visitor);
+    const resultMove = visit(
+        enumValueNode(
+            'action',
+            'move',
+            structValueNode([
+                structFieldValueNode('x', numberValueNode(10)),
+                structFieldValueNode('y', numberValueNode(20)),
+            ]),
+        ),
+        visitor,
+    );
+
+    // Then we expect the values to be resolved from the linkable type as numbers.
+    expect(resultQuit).toStrictEqual({ __kind: 'Quit' });
+    expect(resultWrite).toStrictEqual({ __kind: 'Write', fields: ['Hello'] });
+    expect(resultMove).toStrictEqual({ __kind: 'Move', x: 10, y: 20 });
+});

+ 15 - 0
packages/dynamic-codecs/test/values/MapValueNode.test.ts

@@ -0,0 +1,15 @@
+import { mapEntryValueNode, mapValueNode, numberValueNode, stringValueNode } from '@codama/nodes';
+import { LinkableDictionary, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getValueNodeVisitor } from '../../src';
+
+test('it resolves map value nodes as objects', () => {
+    const node = mapValueNode([
+        mapEntryValueNode(stringValueNode('foo'), numberValueNode(1)),
+        mapEntryValueNode(stringValueNode('bar'), numberValueNode(2)),
+        mapEntryValueNode(stringValueNode('baz'), numberValueNode(3)),
+    ]);
+    const result = visit(node, getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toStrictEqual({ bar: 2, baz: 3, foo: 1 });
+});

+ 10 - 0
packages/dynamic-codecs/test/values/NoneValueNode.test.ts

@@ -0,0 +1,10 @@
+import { noneValueNode } from '@codama/nodes';
+import { LinkableDictionary, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getValueNodeVisitor } from '../../src';
+
+test('it returns a None value object', () => {
+    const result = visit(noneValueNode(), getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toStrictEqual({ __option: 'None' });
+});

+ 10 - 0
packages/dynamic-codecs/test/values/NumberValueNode.test.ts

@@ -0,0 +1,10 @@
+import { numberValueNode } from '@codama/nodes';
+import { LinkableDictionary, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getValueNodeVisitor } from '../../src';
+
+test('it returns the number as-is', () => {
+    const result = visit(numberValueNode(42), getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toBe(42);
+});

+ 11 - 0
packages/dynamic-codecs/test/values/PublicKeyValueNode.test.ts

@@ -0,0 +1,11 @@
+import { publicKeyValueNode } from '@codama/nodes';
+import { LinkableDictionary, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getValueNodeVisitor } from '../../src';
+
+test('it returns the public key as-is', () => {
+    const visitor = getValueNodeVisitor(new LinkableDictionary());
+    const result = visit(publicKeyValueNode('B3SqCE8ww4xmoPcfm1gGibZENPkPCVp3jNwkYcg7xS6j'), visitor);
+    expect(result).toBe('B3SqCE8ww4xmoPcfm1gGibZENPkPCVp3jNwkYcg7xS6j');
+});

+ 11 - 0
packages/dynamic-codecs/test/values/SetValueNode.test.ts

@@ -0,0 +1,11 @@
+import { numberValueNode, setValueNode } from '@codama/nodes';
+import { LinkableDictionary, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getValueNodeVisitor } from '../../src';
+
+test('it returns an array of all resolved value nodes', () => {
+    const node = setValueNode([numberValueNode(1), numberValueNode(2), numberValueNode(3)]);
+    const result = visit(node, getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toStrictEqual([1, 2, 3]);
+});

+ 10 - 0
packages/dynamic-codecs/test/values/SomeValueNode.test.ts

@@ -0,0 +1,10 @@
+import { someValueNode, stringValueNode } from '@codama/nodes';
+import { LinkableDictionary, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getValueNodeVisitor } from '../../src';
+
+test('it wraps the underlying value in a value object', () => {
+    const result = visit(someValueNode(stringValueNode('Hello World!')), getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toStrictEqual({ __option: 'Some', value: 'Hello World!' });
+});

+ 10 - 0
packages/dynamic-codecs/test/values/StringValueNode.test.ts

@@ -0,0 +1,10 @@
+import { stringValueNode } from '@codama/nodes';
+import { LinkableDictionary, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getValueNodeVisitor } from '../../src';
+
+test('it returns the string as-is', () => {
+    const result = visit(stringValueNode('Hello World!'), getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toBe('Hello World!');
+});

+ 14 - 0
packages/dynamic-codecs/test/values/StructValueNode.test.ts

@@ -0,0 +1,14 @@
+import { numberValueNode, stringValueNode, structFieldValueNode, structValueNode } from '@codama/nodes';
+import { LinkableDictionary, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getValueNodeVisitor } from '../../src';
+
+test('it returns struct values as objects', () => {
+    const node = structValueNode([
+        structFieldValueNode('firstname', stringValueNode('John')),
+        structFieldValueNode('age', numberValueNode(42)),
+    ]);
+    const result = visit(node, getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toStrictEqual({ age: 42, firstname: 'John' });
+});

+ 11 - 0
packages/dynamic-codecs/test/values/TupleValueNode.test.ts

@@ -0,0 +1,11 @@
+import { booleanValueNode, numberValueNode, stringValueNode, tupleValueNode } from '@codama/nodes';
+import { LinkableDictionary, visit } from '@codama/visitors-core';
+import { expect, test } from 'vitest';
+
+import { getValueNodeVisitor } from '../../src';
+
+test('it returns the tuple as an array of values', () => {
+    const node = tupleValueNode([numberValueNode(42), stringValueNode('Hello'), booleanValueNode(true)]);
+    const result = visit(node, getValueNodeVisitor(new LinkableDictionary()));
+    expect(result).toStrictEqual([42, 'Hello', true]);
+});

+ 10 - 0
packages/dynamic-codecs/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-codecs/tsconfig.json

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

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

@@ -31,6 +31,9 @@ export const CODAMA_ERROR__UNEXPECTED_NESTED_NODE_KIND = 3 as const;
 export const CODAMA_ERROR__LINKED_NODE_NOT_FOUND = 4 as const;
 export const CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE = 5 as const;
 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;
 
 // Visitors-related errors.
 // Reserve error codes in the range [1200000-1200999].
@@ -47,6 +50,7 @@ export const CODAMA_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE = 1200008 as const;
 export const CODAMA_ERROR__VISITORS__INSTRUCTION_ENUM_ARGUMENT_NOT_FOUND = 1200009 as const;
 export const CODAMA_ERROR__VISITORS__CANNOT_FLATTEN_STRUCT_WITH_CONFLICTING_ATTRIBUTES = 1200010 as const;
 export const CODAMA_ERROR__VISITORS__RENDER_MAP_KEY_NOT_FOUND = 1200011 as const;
+export const CODAMA_ERROR__VISITORS__CANNOT_REMOVE_LAST_PATH_IN_NODE_STACK = 1200012 as const;
 
 // Anchor-related errors.
 // Reserve error codes in the range [2100000-2100999].
@@ -81,17 +85,21 @@ 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__ENUM_VARIANT_NOT_FOUND
     | typeof CODAMA_ERROR__LINKED_NODE_NOT_FOUND
     | typeof CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE
     | typeof CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE
     | typeof CODAMA_ERROR__UNEXPECTED_NESTED_NODE_KIND
     | typeof CODAMA_ERROR__UNEXPECTED_NODE_KIND
+    | typeof CODAMA_ERROR__UNRECOGNIZED_BYTES_ENCODING
     | typeof CODAMA_ERROR__UNRECOGNIZED_NODE_KIND
+    | typeof CODAMA_ERROR__UNRECOGNIZED_NUMBER_FORMAT
     | typeof CODAMA_ERROR__VERSION_MISMATCH
     | typeof CODAMA_ERROR__VISITORS__ACCOUNT_FIELD_NOT_FOUND
     | typeof CODAMA_ERROR__VISITORS__CANNOT_ADD_DUPLICATED_PDA_NAMES
     | typeof CODAMA_ERROR__VISITORS__CANNOT_EXTEND_MISSING_VISIT_FUNCTION
     | typeof CODAMA_ERROR__VISITORS__CANNOT_FLATTEN_STRUCT_WITH_CONFLICTING_ATTRIBUTES
+    | typeof CODAMA_ERROR__VISITORS__CANNOT_REMOVE_LAST_PATH_IN_NODE_STACK
     | typeof CODAMA_ERROR__VISITORS__CANNOT_USE_OPTIONAL_ACCOUNT_AS_PDA_SEED_VALUE
     | typeof CODAMA_ERROR__VISITORS__CYCLIC_DEPENDENCY_DETECTED_WHEN_RESOLVING_INSTRUCTION_DEFAULT_VALUES
     | typeof CODAMA_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE

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

@@ -7,6 +7,7 @@ import {
     AccountNode,
     AccountValueNode,
     CamelCaseString,
+    EnumTypeNode,
     InstructionAccountNode,
     InstructionArgumentNode,
     InstructionNode,
@@ -24,17 +25,21 @@ import {
     CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED,
     CODAMA_ERROR__ANCHOR__TYPE_PATH_MISSING,
     CODAMA_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE,
+    CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND,
     CODAMA_ERROR__LINKED_NODE_NOT_FOUND,
     CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE,
     CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE,
     CODAMA_ERROR__UNEXPECTED_NESTED_NODE_KIND,
     CODAMA_ERROR__UNEXPECTED_NODE_KIND,
+    CODAMA_ERROR__UNRECOGNIZED_BYTES_ENCODING,
     CODAMA_ERROR__UNRECOGNIZED_NODE_KIND,
+    CODAMA_ERROR__UNRECOGNIZED_NUMBER_FORMAT,
     CODAMA_ERROR__VERSION_MISMATCH,
     CODAMA_ERROR__VISITORS__ACCOUNT_FIELD_NOT_FOUND,
     CODAMA_ERROR__VISITORS__CANNOT_ADD_DUPLICATED_PDA_NAMES,
     CODAMA_ERROR__VISITORS__CANNOT_EXTEND_MISSING_VISIT_FUNCTION,
     CODAMA_ERROR__VISITORS__CANNOT_FLATTEN_STRUCT_WITH_CONFLICTING_ATTRIBUTES,
+    CODAMA_ERROR__VISITORS__CANNOT_REMOVE_LAST_PATH_IN_NODE_STACK,
     CODAMA_ERROR__VISITORS__CANNOT_USE_OPTIONAL_ACCOUNT_AS_PDA_SEED_VALUE,
     CODAMA_ERROR__VISITORS__CYCLIC_DEPENDENCY_DETECTED_WHEN_RESOLVING_INSTRUCTION_DEFAULT_VALUES,
     CODAMA_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE,
@@ -71,6 +76,11 @@ export type CodamaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
     [CODAMA_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE]: {
         idlType: string;
     };
+    [CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND]: {
+        enum: EnumTypeNode;
+        enumName: CamelCaseString;
+        variant: CamelCaseString;
+    };
     [CODAMA_ERROR__LINKED_NODE_NOT_FOUND]: {
         kind: LinkNode['kind'];
         linkNode: LinkNode;
@@ -94,9 +104,15 @@ export type CodamaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
         kind: NodeKind | null;
         node: Node | null | undefined;
     };
+    [CODAMA_ERROR__UNRECOGNIZED_BYTES_ENCODING]: {
+        encoding: string;
+    };
     [CODAMA_ERROR__UNRECOGNIZED_NODE_KIND]: {
         kind: string;
     };
+    [CODAMA_ERROR__UNRECOGNIZED_NUMBER_FORMAT]: {
+        format: string;
+    };
     [CODAMA_ERROR__VERSION_MISMATCH]: {
         codamaVersion: string;
         rootVersion: string;
@@ -117,6 +133,9 @@ export type CodamaErrorContext = DefaultUnspecifiedErrorContextToUndefined<{
     [CODAMA_ERROR__VISITORS__CANNOT_FLATTEN_STRUCT_WITH_CONFLICTING_ATTRIBUTES]: {
         conflictingAttributes: CamelCaseString[];
     };
+    [CODAMA_ERROR__VISITORS__CANNOT_REMOVE_LAST_PATH_IN_NODE_STACK]: {
+        path: readonly Node[];
+    };
     [CODAMA_ERROR__VISITORS__CANNOT_USE_OPTIONAL_ACCOUNT_AS_PDA_SEED_VALUE]: {
         instruction: InstructionNode;
         instructionAccount: InstructionAccountNode;

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

@@ -9,17 +9,21 @@ import {
     CODAMA_ERROR__ANCHOR__SEED_KIND_UNIMPLEMENTED,
     CODAMA_ERROR__ANCHOR__TYPE_PATH_MISSING,
     CODAMA_ERROR__ANCHOR__UNRECOGNIZED_IDL_TYPE,
+    CODAMA_ERROR__ENUM_VARIANT_NOT_FOUND,
     CODAMA_ERROR__LINKED_NODE_NOT_FOUND,
     CODAMA_ERROR__NODE_FILESYSTEM_FUNCTION_UNAVAILABLE,
     CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE,
     CODAMA_ERROR__UNEXPECTED_NESTED_NODE_KIND,
     CODAMA_ERROR__UNEXPECTED_NODE_KIND,
+    CODAMA_ERROR__UNRECOGNIZED_BYTES_ENCODING,
     CODAMA_ERROR__UNRECOGNIZED_NODE_KIND,
+    CODAMA_ERROR__UNRECOGNIZED_NUMBER_FORMAT,
     CODAMA_ERROR__VERSION_MISMATCH,
     CODAMA_ERROR__VISITORS__ACCOUNT_FIELD_NOT_FOUND,
     CODAMA_ERROR__VISITORS__CANNOT_ADD_DUPLICATED_PDA_NAMES,
     CODAMA_ERROR__VISITORS__CANNOT_EXTEND_MISSING_VISIT_FUNCTION,
     CODAMA_ERROR__VISITORS__CANNOT_FLATTEN_STRUCT_WITH_CONFLICTING_ATTRIBUTES,
+    CODAMA_ERROR__VISITORS__CANNOT_REMOVE_LAST_PATH_IN_NODE_STACK,
     CODAMA_ERROR__VISITORS__CANNOT_USE_OPTIONAL_ACCOUNT_AS_PDA_SEED_VALUE,
     CODAMA_ERROR__VISITORS__CYCLIC_DEPENDENCY_DETECTED_WHEN_RESOLVING_INSTRUCTION_DEFAULT_VALUES,
     CODAMA_ERROR__VISITORS__FAILED_TO_VALIDATE_NODE,
@@ -45,13 +49,16 @@ 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__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]:
         'Node.js filesystem function [$fsFunction] is not available in your environment.',
     [CODAMA_ERROR__RENDERERS__UNSUPPORTED_NODE]: 'Cannot render the encountered node of kind [$kind].',
     [CODAMA_ERROR__UNEXPECTED_NESTED_NODE_KIND]: 'Expected nested node of kind [$expectedKinds], got [$kind]',
     [CODAMA_ERROR__UNEXPECTED_NODE_KIND]: 'Expected node of kind [$expectedKinds], got [$kind].',
+    [CODAMA_ERROR__UNRECOGNIZED_BYTES_ENCODING]: 'Unrecognized bytes encoding [$encoding].',
     [CODAMA_ERROR__UNRECOGNIZED_NODE_KIND]: 'Unrecognized node kind [$kind].',
+    [CODAMA_ERROR__UNRECOGNIZED_NUMBER_FORMAT]: 'Unrecognized number format [$format].',
     [CODAMA_ERROR__VERSION_MISMATCH]:
         'The provided RootNode version [$rootVersion] is not compatible with the installed Codama version [$codamaVersion].',
     [CODAMA_ERROR__VISITORS__ACCOUNT_FIELD_NOT_FOUND]: 'Account [$name] does not have a field named [$missingField].',
@@ -61,6 +68,7 @@ export const CodamaErrorMessages: Readonly<{
         'Cannot extend visitor with function [$visitFunction] as the base visitor does not support it.',
     [CODAMA_ERROR__VISITORS__CANNOT_FLATTEN_STRUCT_WITH_CONFLICTING_ATTRIBUTES]:
         'Cannot flatten struct since this would cause the following attributes to conflict [$conflictingAttributes].',
+    [CODAMA_ERROR__VISITORS__CANNOT_REMOVE_LAST_PATH_IN_NODE_STACK]: 'Cannot remove the last path in the node stack.',
     [CODAMA_ERROR__VISITORS__CANNOT_USE_OPTIONAL_ACCOUNT_AS_PDA_SEED_VALUE]:
         'Cannot use optional account [$seedValueName] as the [$seedName] PDA seed for the [$instructionAccountName] account of the [$instructionName] instruction.',
     [CODAMA_ERROR__VISITORS__CYCLIC_DEPENDENCY_DETECTED_WHEN_RESOLVING_INSTRUCTION_DEFAULT_VALUES]:

+ 5 - 3
packages/visitors-core/src/NodeStack.ts

@@ -1,3 +1,4 @@
+import { CODAMA_ERROR__VISITORS__CANNOT_REMOVE_LAST_PATH_IN_NODE_STACK, CodamaError } from '@codama/errors';
 import { GetNodeFromKind, Node, NodeKind } from '@codama/nodes';
 
 import { assertIsNodePath, NodePath } from './NodePath';
@@ -45,9 +46,10 @@ export class NodeStack {
     }
 
     public popPath(): NodePath {
-        if (this.stack.length === 0) {
-            // TODO: Coded error
-            throw new Error('The stack of paths can never be empty.');
+        if (this.stack.length <= 1) {
+            throw new CodamaError(CODAMA_ERROR__VISITORS__CANNOT_REMOVE_LAST_PATH_IN_NODE_STACK, {
+                path: [...this.stack[this.stack.length - 1]],
+            });
         }
         return [...this.stack.pop()!];
     }

+ 116 - 0
pnpm-lock.yaml

@@ -75,6 +75,21 @@ importers:
         specifier: ^8.1.9
         version: 8.1.9
 
+  packages/dynamic-codecs:
+    dependencies:
+      '@codama/errors':
+        specifier: workspace:*
+        version: link:../errors
+      '@codama/nodes':
+        specifier: workspace:*
+        version: link:../nodes
+      '@codama/visitors-core':
+        specifier: workspace:*
+        version: link:../visitors-core
+      '@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':
@@ -819,12 +834,32 @@ packages:
   '@solana/codecs-core@2.0.0-rc.2':
     resolution: {integrity: sha512-UHSdAmbiFYuCwb2UmYZkOIj2oTKuYqje1LZ1VWCdnpp2qf+XAGbxIHyL/auV91/nK8iggL9417BMS2Q3AfMzFg==}
     engines: {node: '>=20.18.0'}
+    deprecated: This version introduced a bug that broke RPC subscriptions. Please use 2.0.0-rc.3.
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/codecs-core@2.0.0-rc.4':
+    resolution: {integrity: sha512-JIrTSps032mSE3wBxW3bXOqWfoy4CMy1CX/XeVCijyh5kLVxZTSDIdRTYdePdL1yzaOZF1Xysvt1DhOUgBdM+A==}
+    engines: {node: '>=20.18.0'}
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/codecs-data-structures@2.0.0-rc.4':
+    resolution: {integrity: sha512-smF4Z4WCbr3ppoZhhT7/e5XMG6VFSHFPDLsayt4aHUvP1clZAew5uOy0qLY0qdxbttSmfoxXqf2SUFpJw8Jadg==}
+    engines: {node: '>=20.18.0'}
     peerDependencies:
       typescript: '>=5'
 
   '@solana/codecs-numbers@2.0.0-rc.2':
     resolution: {integrity: sha512-cQoTXVYF+nQjIh6VtrD4i7rfyJL0q46TMOd64+J/+2bQG2UT4Cwwo0b2lHuqtQr+2+BYjHPFDSNLaa3IWpPNog==}
     engines: {node: '>=20.18.0'}
+    deprecated: This version introduced a bug that broke RPC subscriptions. Please use 2.0.0-rc.3.
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/codecs-numbers@2.0.0-rc.4':
+    resolution: {integrity: sha512-ZJR7TaUO65+3Hzo3YOOUCS0wlzh17IW+j0MZC2LCk1R0woaypRpHKj4iSMYeQOZkMxsd9QT3WNvjFrPC2qA6Sw==}
+    engines: {node: '>=20.18.0'}
     peerDependencies:
       typescript: '>=5'
 
@@ -835,9 +870,30 @@ packages:
       fastestsmallesttextencoderdecoder: ^1.0.22
       typescript: '>=5'
 
+  '@solana/codecs-strings@2.0.0-rc.4':
+    resolution: {integrity: sha512-LGfK2RL0BKjYYUfzu2FG/gTgCsYOMz9FKVs2ntji6WneZygPxJTV5W98K3J8Rl0JewpCSCFQH3xjLSHBJUS0fA==}
+    engines: {node: '>=20.18.0'}
+    peerDependencies:
+      fastestsmallesttextencoderdecoder: ^1.0.22
+      typescript: '>=5'
+
+  '@solana/codecs@2.0.0-rc.4':
+    resolution: {integrity: sha512-h9GQGYLfBifzLhyZuef5FUaZGxLW7JNLDlEYCErA7x7Ty2ssF98sswsLsWKcbv5Cz1QsW7A6xGv4PCjvIDOCxQ==}
+    engines: {node: '>=20.18.0'}
+    peerDependencies:
+      typescript: '>=5'
+
   '@solana/errors@2.0.0-rc.2':
     resolution: {integrity: sha512-2NPRQYOLwpwP2J4KC0+Vc+qmwTGcu6VrKA9iRBLP0axR+49er9AqpoK3EowTmt0BvAvyArOJRuaPRIFMQI2IdA==}
     engines: {node: '>=20.18.0'}
+    deprecated: This version introduced a bug that broke RPC subscriptions. Please use 2.0.0-rc.3.
+    hasBin: true
+    peerDependencies:
+      typescript: '>=5'
+
+  '@solana/errors@2.0.0-rc.4':
+    resolution: {integrity: sha512-0PPaMyB81keEHG/1pnyEuiBVKctbXO641M2w3CIOrYT/wzjunfF0FTxsqq9wYJeYo0AyiefCKGgSPs6wiY2PpQ==}
+    engines: {node: '>=20.18.0'}
     hasBin: true
     peerDependencies:
       typescript: '>=5'
@@ -855,6 +911,12 @@ packages:
       eslint-plugin-typescript-sort-keys: ^3.2.0
       typescript: ^5.1.6
 
+  '@solana/options@2.0.0-rc.4':
+    resolution: {integrity: sha512-5W8aswMBhcdv2pD5lHLdHIZ98ymhQNBmeFncEoVZLTrshf7KqyxZ8xtILcWNCUgOev1+yp9hMTNV9SEgrgyNrQ==}
+    engines: {node: '>=20.18.0'}
+    peerDependencies:
+      typescript: '>=5'
+
   '@solana/prettier-config-solana@0.0.5':
     resolution: {integrity: sha512-igtLH1QaX5xzSLlqteexRIg9X1QKA03xKYQc2qY1TrMDDhxKXoRZOStQPWdita2FVJzxTGz/tdMGC1vS0biRcg==}
     peerDependencies:
@@ -2841,12 +2903,30 @@ snapshots:
       '@solana/errors': 2.0.0-rc.2(typescript@5.6.3)
       typescript: 5.6.3
 
+  '@solana/codecs-core@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/codecs-data-structures@2.0.0-rc.4(typescript@5.6.3)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-rc.4(typescript@5.6.3)
+      '@solana/codecs-numbers': 2.0.0-rc.4(typescript@5.6.3)
+      '@solana/errors': 2.0.0-rc.4(typescript@5.6.3)
+      typescript: 5.6.3
+
   '@solana/codecs-numbers@2.0.0-rc.2(typescript@5.6.3)':
     dependencies:
       '@solana/codecs-core': 2.0.0-rc.2(typescript@5.6.3)
       '@solana/errors': 2.0.0-rc.2(typescript@5.6.3)
       typescript: 5.6.3
 
+  '@solana/codecs-numbers@2.0.0-rc.4(typescript@5.6.3)':
+    dependencies:
+      '@solana/codecs-core': 2.0.0-rc.4(typescript@5.6.3)
+      '@solana/errors': 2.0.0-rc.4(typescript@5.6.3)
+      typescript: 5.6.3
+
   '@solana/codecs-strings@2.0.0-rc.2(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)':
     dependencies:
       '@solana/codecs-core': 2.0.0-rc.2(typescript@5.6.3)
@@ -2855,12 +2935,37 @@ snapshots:
       fastestsmallesttextencoderdecoder: 1.0.22
       typescript: 5.6.3
 
+  '@solana/codecs-strings@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)
+      '@solana/codecs-numbers': 2.0.0-rc.4(typescript@5.6.3)
+      '@solana/errors': 2.0.0-rc.4(typescript@5.6.3)
+      fastestsmallesttextencoderdecoder: 1.0.22
+      typescript: 5.6.3
+
+  '@solana/codecs@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)
+      '@solana/codecs-data-structures': 2.0.0-rc.4(typescript@5.6.3)
+      '@solana/codecs-numbers': 2.0.0-rc.4(typescript@5.6.3)
+      '@solana/codecs-strings': 2.0.0-rc.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)
+      '@solana/options': 2.0.0-rc.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)
+      typescript: 5.6.3
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+
   '@solana/errors@2.0.0-rc.2(typescript@5.6.3)':
     dependencies:
       chalk: 5.3.0
       commander: 12.1.0
       typescript: 5.6.3
 
+  '@solana/errors@2.0.0-rc.4(typescript@5.6.3)':
+    dependencies:
+      chalk: 5.3.0
+      commander: 12.1.0
+      typescript: 5.6.3
+
   '@solana/eslint-config-solana@3.0.3(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-jest@27.9.0(@typescript-eslint/eslint-plugin@6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3))(eslint-plugin-react-hooks@4.6.0(eslint@8.57.1))(eslint-plugin-simple-import-sort@10.0.0(eslint@8.57.1))(eslint-plugin-sort-keys-fix@1.1.2)(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))(eslint@8.57.1)(typescript@5.6.3)':
     dependencies:
       '@typescript-eslint/eslint-plugin': 6.21.0(@typescript-eslint/parser@6.21.0(eslint@8.57.1)(typescript@5.6.3))(eslint@8.57.1)(typescript@5.6.3)
@@ -2873,6 +2978,17 @@ 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/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)
+      '@solana/codecs-data-structures': 2.0.0-rc.4(typescript@5.6.3)
+      '@solana/codecs-numbers': 2.0.0-rc.4(typescript@5.6.3)
+      '@solana/codecs-strings': 2.0.0-rc.4(fastestsmallesttextencoderdecoder@1.0.22)(typescript@5.6.3)
+      '@solana/errors': 2.0.0-rc.4(typescript@5.6.3)
+      typescript: 5.6.3
+    transitivePeerDependencies:
+      - fastestsmallesttextencoderdecoder
+
   '@solana/prettier-config-solana@0.0.5(prettier@3.3.3)':
     dependencies:
       prettier: 3.3.3