diff --git a/.changeset/witty-wasps-look.md b/.changeset/witty-wasps-look.md new file mode 100644 index 000000000..217d04411 --- /dev/null +++ b/.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 diff --git a/packages/dynamic-codecs/.gitignore b/packages/dynamic-codecs/.gitignore new file mode 100644 index 000000000..849ddff3b --- /dev/null +++ b/packages/dynamic-codecs/.gitignore @@ -0,0 +1 @@ +dist/ diff --git a/packages/dynamic-codecs/.prettierignore b/packages/dynamic-codecs/.prettierignore new file mode 100644 index 000000000..3d73fdedb --- /dev/null +++ b/packages/dynamic-codecs/.prettierignore @@ -0,0 +1,5 @@ +dist/ +e2e/ +test-ledger/ +target/ +CHANGELOG.md diff --git a/packages/dynamic-codecs/LICENSE b/packages/dynamic-codecs/LICENSE new file mode 100644 index 000000000..22fd7b02c --- /dev/null +++ b/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. diff --git a/packages/dynamic-codecs/README.md b/packages/dynamic-codecs/README.md new file mode 100644 index 000000000..b68840b1f --- /dev/null +++ b/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` (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` 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)); +``` diff --git a/packages/dynamic-codecs/package.json b/packages/dynamic-codecs/package.json new file mode 100644 index 000000000..3909f0d82 --- /dev/null +++ b/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" + ] +} diff --git a/packages/dynamic-codecs/src/codecs.ts b/packages/dynamic-codecs/src/codecs.ts new file mode 100644 index 000000000..686ef3972 --- /dev/null +++ b/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, options: CodecVisitorOptions = {}): Codec { + 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, EncodableNodes['kind']> { + const stack = options.stack ?? new NodeStack(); + const bytesEncoding = options.bytesEncoding ?? 'base64'; + const valueNodeVisitor = getValueNodeVisitor(linkables, { + codecVisitorFactory: () => visitor, + stack, + }); + + const baseVisitor: Visitor, 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; + }, + visitBooleanType(node) { + const size = visit(node.size, this) as NumberCodec; + return getBooleanCodec({ size }) as Codec; + }, + 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; + }, + 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; + }, + 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; + } + // 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; + }, + 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): object => Object.fromEntries(map), + ) as Codec; + }, + visitNumberType(node) { + return getCodecFromNumberFormat(node.format) as Codec; + }, + 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; + }, + 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; + }, + 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; + }, + 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; + }, + visitTupleType(node) { + const items = node.items.map(item => visit(item, this)); + return getTupleCodec(items) as Codec; + }, + 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, +): NumberCodec | number | 'remainder' { + if (isNode(node, 'prefixedCountNode')) { + return visit(node.prefix, visitor) as NumberCodec; + } + if (isNode(node, 'fixedCountNode')) { + return node.value; + } + return 'remainder'; +} diff --git a/packages/dynamic-codecs/src/index.ts b/packages/dynamic-codecs/src/index.ts new file mode 100644 index 000000000..b6ae14ec5 --- /dev/null +++ b/packages/dynamic-codecs/src/index.ts @@ -0,0 +1,2 @@ +export * from './codecs'; +export * from './values'; diff --git a/packages/dynamic-codecs/src/types/global.d.ts b/packages/dynamic-codecs/src/types/global.d.ts new file mode 100644 index 000000000..13de8a7ce --- /dev/null +++ b/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; diff --git a/packages/dynamic-codecs/src/values.ts b/packages/dynamic-codecs/src/values.ts new file mode 100644 index 000000000..48689dbe3 --- /dev/null +++ b/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; + codecVisitorOptions?: CodecVisitorOptions; + stack?: NodeStack; + } = {}, +): Visitor { + const stack = options.stack ?? new NodeStack(); + let cachedCodecVisitor: ReturnType | null = null; + const codecVisitorFactory = + options.codecVisitorFactory ?? + (() => (cachedCodecVisitor ??= getNodeCodecVisitor(linkables, { stack, ...options.codecVisitorOptions }))); + + const baseVisitor: Visitor = { + 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)); +} diff --git a/packages/dynamic-codecs/test/_setup.ts b/packages/dynamic-codecs/test/_setup.ts new file mode 100644 index 000000000..def1575a5 --- /dev/null +++ b/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); +} diff --git a/packages/dynamic-codecs/test/codecs/AccountNode.test.ts b/packages/dynamic-codecs/test/codecs/AccountNode.test.ts new file mode 100644 index 000000000..5a01e40e4 --- /dev/null +++ b/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 }); +}); diff --git a/packages/dynamic-codecs/test/codecs/ArrayTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/ArrayTypeNode.test.ts new file mode 100644 index 000000000..3d913f8b9 --- /dev/null +++ b/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]); +}); diff --git a/packages/dynamic-codecs/test/codecs/BooleanTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/BooleanTypeNode.test.ts new file mode 100644 index 000000000..dbe671313 --- /dev/null +++ b/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); +}); diff --git a/packages/dynamic-codecs/test/codecs/BytesTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/BytesTypeNode.test.ts new file mode 100644 index 000000000..5a114728b --- /dev/null +++ b/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=']); +}); diff --git a/packages/dynamic-codecs/test/codecs/DefinedTypeLinkNode.test.ts b/packages/dynamic-codecs/test/codecs/DefinedTypeLinkNode.test.ts new file mode 100644 index 000000000..bfd1886ea --- /dev/null +++ b/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); +}); diff --git a/packages/dynamic-codecs/test/codecs/DefinedTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/DefinedTypeNode.test.ts new file mode 100644 index 000000000..883081f0d --- /dev/null +++ b/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); +}); diff --git a/packages/dynamic-codecs/test/codecs/EnumTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/EnumTypeNode.test.ts new file mode 100644 index 000000000..200676dcb --- /dev/null +++ b/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); +}); diff --git a/packages/dynamic-codecs/test/codecs/FixedSizeTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/FixedSizeTypeNode.test.ts new file mode 100644 index 000000000..7cd15fd83 --- /dev/null +++ b/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'); +}); diff --git a/packages/dynamic-codecs/test/codecs/HiddenPrefixTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/HiddenPrefixTypeNode.test.ts new file mode 100644 index 000000000..3994bfa1a --- /dev/null +++ b/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'); +}); diff --git a/packages/dynamic-codecs/test/codecs/HiddenSuffixTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/HiddenSuffixTypeNode.test.ts new file mode 100644 index 000000000..865d7cbad --- /dev/null +++ b/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'); +}); diff --git a/packages/dynamic-codecs/test/codecs/InstructionArgumentNode.test.ts b/packages/dynamic-codecs/test/codecs/InstructionArgumentNode.test.ts new file mode 100644 index 000000000..c672e43ab --- /dev/null +++ b/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); +}); diff --git a/packages/dynamic-codecs/test/codecs/InstructionNode.test.ts b/packages/dynamic-codecs/test/codecs/InstructionNode.test.ts new file mode 100644 index 000000000..1568b75d2 --- /dev/null +++ b/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 }); +}); diff --git a/packages/dynamic-codecs/test/codecs/MapTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/MapTypeNode.test.ts new file mode 100644 index 000000000..3d9a00dc8 --- /dev/null +++ b/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); +}); diff --git a/packages/dynamic-codecs/test/codecs/NumberTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/NumberTypeNode.test.ts new file mode 100644 index 000000000..b59d2bc48 --- /dev/null +++ b/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); +}); diff --git a/packages/dynamic-codecs/test/codecs/OptionTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/OptionTypeNode.test.ts new file mode 100644 index 000000000..8ea52c7c2 --- /dev/null +++ b/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' }); +}); diff --git a/packages/dynamic-codecs/test/codecs/PostOffsetTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/PostOffsetTypeNode.test.ts new file mode 100644 index 000000000..770473407 --- /dev/null +++ b/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]); +}); diff --git a/packages/dynamic-codecs/test/codecs/PreOffsetTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/PreOffsetTypeNode.test.ts new file mode 100644 index 000000000..10c710697 --- /dev/null +++ b/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]); +}); diff --git a/packages/dynamic-codecs/test/codecs/PublicKeyTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/PublicKeyTypeNode.test.ts new file mode 100644 index 000000000..8655185be --- /dev/null +++ b/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', + ); +}); diff --git a/packages/dynamic-codecs/test/codecs/RemainderOptionTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/RemainderOptionTypeNode.test.ts new file mode 100644 index 000000000..8fdbcb9c8 --- /dev/null +++ b/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' }); +}); diff --git a/packages/dynamic-codecs/test/codecs/SentinelTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/SentinelTypeNode.test.ts new file mode 100644 index 000000000..3dcfd940f --- /dev/null +++ b/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!'); +}); diff --git a/packages/dynamic-codecs/test/codecs/SetTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/SetTypeNode.test.ts new file mode 100644 index 000000000..c60d6f176 --- /dev/null +++ b/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]); +}); diff --git a/packages/dynamic-codecs/test/codecs/SizePrefixTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/SizePrefixTypeNode.test.ts new file mode 100644 index 000000000..37860749a --- /dev/null +++ b/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!'); +}); diff --git a/packages/dynamic-codecs/test/codecs/StringTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/StringTypeNode.test.ts new file mode 100644 index 000000000..af05b6c08 --- /dev/null +++ b/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!'); +}); diff --git a/packages/dynamic-codecs/test/codecs/StructFieldTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/StructFieldTypeNode.test.ts new file mode 100644 index 000000000..c998c8e3e --- /dev/null +++ b/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); +}); diff --git a/packages/dynamic-codecs/test/codecs/StructTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/StructTypeNode.test.ts new file mode 100644 index 000000000..e806c584a --- /dev/null +++ b/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); +}); diff --git a/packages/dynamic-codecs/test/codecs/TupleTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/TupleTypeNode.test.ts new file mode 100644 index 000000000..a69575460 --- /dev/null +++ b/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]); +}); diff --git a/packages/dynamic-codecs/test/codecs/ZeroableOptionTypeNode.test.ts b/packages/dynamic-codecs/test/codecs/ZeroableOptionTypeNode.test.ts new file mode 100644 index 000000000..40538bd98 --- /dev/null +++ b/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' }); +}); diff --git a/packages/dynamic-codecs/test/types/global.d.ts b/packages/dynamic-codecs/test/types/global.d.ts new file mode 100644 index 000000000..13de8a7ce --- /dev/null +++ b/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; diff --git a/packages/dynamic-codecs/test/values/ArrayValueNode.test.ts b/packages/dynamic-codecs/test/values/ArrayValueNode.test.ts new file mode 100644 index 000000000..0fa97cc20 --- /dev/null +++ b/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]); +}); diff --git a/packages/dynamic-codecs/test/values/BytesValueNode.test.ts b/packages/dynamic-codecs/test/values/BytesValueNode.test.ts new file mode 100644 index 000000000..fc2018ee9 --- /dev/null +++ b/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']); +}); diff --git a/packages/dynamic-codecs/test/values/ConstantValueNode.test.ts b/packages/dynamic-codecs/test/values/ConstantValueNode.test.ts new file mode 100644 index 000000000..ebc32ba7f --- /dev/null +++ b/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']); +}); diff --git a/packages/dynamic-codecs/test/values/EnumValueNode.test.ts b/packages/dynamic-codecs/test/values/EnumValueNode.test.ts new file mode 100644 index 000000000..551a1990d --- /dev/null +++ b/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 }); +}); diff --git a/packages/dynamic-codecs/test/values/MapValueNode.test.ts b/packages/dynamic-codecs/test/values/MapValueNode.test.ts new file mode 100644 index 000000000..fdec84cfc --- /dev/null +++ b/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 }); +}); diff --git a/packages/dynamic-codecs/test/values/NoneValueNode.test.ts b/packages/dynamic-codecs/test/values/NoneValueNode.test.ts new file mode 100644 index 000000000..6cb0c4d91 --- /dev/null +++ b/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' }); +}); diff --git a/packages/dynamic-codecs/test/values/NumberValueNode.test.ts b/packages/dynamic-codecs/test/values/NumberValueNode.test.ts new file mode 100644 index 000000000..a820b6be6 --- /dev/null +++ b/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); +}); diff --git a/packages/dynamic-codecs/test/values/PublicKeyValueNode.test.ts b/packages/dynamic-codecs/test/values/PublicKeyValueNode.test.ts new file mode 100644 index 000000000..46a0d6881 --- /dev/null +++ b/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'); +}); diff --git a/packages/dynamic-codecs/test/values/SetValueNode.test.ts b/packages/dynamic-codecs/test/values/SetValueNode.test.ts new file mode 100644 index 000000000..13c35b00c --- /dev/null +++ b/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]); +}); diff --git a/packages/dynamic-codecs/test/values/SomeValueNode.test.ts b/packages/dynamic-codecs/test/values/SomeValueNode.test.ts new file mode 100644 index 000000000..bba1859d5 --- /dev/null +++ b/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!' }); +}); diff --git a/packages/dynamic-codecs/test/values/StringValueNode.test.ts b/packages/dynamic-codecs/test/values/StringValueNode.test.ts new file mode 100644 index 000000000..3ca0a0260 --- /dev/null +++ b/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!'); +}); diff --git a/packages/dynamic-codecs/test/values/StructValueNode.test.ts b/packages/dynamic-codecs/test/values/StructValueNode.test.ts new file mode 100644 index 000000000..26c8daaa7 --- /dev/null +++ b/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' }); +}); diff --git a/packages/dynamic-codecs/test/values/TupleValueNode.test.ts b/packages/dynamic-codecs/test/values/TupleValueNode.test.ts new file mode 100644 index 000000000..26f6215df --- /dev/null +++ b/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]); +}); diff --git a/packages/dynamic-codecs/tsconfig.declarations.json b/packages/dynamic-codecs/tsconfig.declarations.json new file mode 100644 index 000000000..dc2d27bb0 --- /dev/null +++ b/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"] +} diff --git a/packages/dynamic-codecs/tsconfig.json b/packages/dynamic-codecs/tsconfig.json new file mode 100644 index 000000000..45b99721c --- /dev/null +++ b/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"] +} diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 5ef16de83..1d612f161 100644 --- a/packages/errors/src/codes.ts +++ b/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 diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index 08dd224c9..2e57f5775 100644 --- a/packages/errors/src/context.ts +++ b/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; diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index 833c94f00..fa849272d 100644 --- a/packages/errors/src/messages.ts +++ b/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]: diff --git a/packages/visitors-core/src/NodeStack.ts b/packages/visitors-core/src/NodeStack.ts index 0f0fa4d33..944f3fa5c 100644 --- a/packages/visitors-core/src/NodeStack.ts +++ b/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()!]; } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 66153e924..e15488b92 100644 --- a/pnpm-lock.yaml +++ b/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