Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions .changeset/brave-keys-dance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
"@evolution-sdk/evolution": patch
---

Improve `Variant` type inference with `PropertyKey` constraint

The `Variant` helper now accepts `PropertyKey` (string | number | symbol) as variant keys instead of just strings, enabling more flexible discriminated union patterns.

**Before:**
```typescript
// Only string keys were properly typed
const MyVariant = TSchema.Variant({
"Success": { value: TSchema.Integer },
"Error": { message: TSchema.ByteArray }
})
```

**After:**
```typescript
// Now supports symbols and numbers as variant keys
const MyVariant = TSchema.Variant({
Success: { value: TSchema.Integer },
Error: { message: TSchema.ByteArray }
})
// Type inference is improved, especially with const assertions
```

Replace `@ts-expect-error` with `as any` following Effect patterns

Improved code quality by replacing forbidden `@ts-expect-error` directives with explicit `as any` type assertions, consistent with Effect Schema's approach for dynamic object construction.

Add comprehensive Cardano Address type support

Added full CBOR encoding support for Cardano address structures with Aiken compatibility:

```typescript
const Credential = TSchema.Variant({
VerificationKey: { hash: TSchema.ByteArray },
Script: { hash: TSchema.ByteArray }
})

const Address = TSchema.Struct({
payment_credential: Credential,
stake_credential: TSchema.UndefinedOr(
TSchema.Variant({
Inline: { credential: Credential },
Pointer: {
slot_number: TSchema.Integer,
transaction_index: TSchema.Integer,
certificate_index: TSchema.Integer
}
})
)
})

// Creates proper CBOR encoding matching Aiken's output
const address = Data.withSchema(Address).toData({
payment_credential: { VerificationKey: { hash } },
stake_credential: { Inline: { credential: { VerificationKey: { stakeHash } } } }
})
```
5 changes: 5 additions & 0 deletions .changeset/silent-forks-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@evolution-sdk/evolution": patch
---

Add Aiken-compatible CBOR encoding with encodeMapAsPairs option and comprehensive test suite. PlutusData maps can now encode as arrays of pairs (Aiken style) or CBOR maps (CML style). Includes 72 Aiken reference tests and 40 TypeScript compatibility tests verifying identical encoding. Also fixes branded schema pattern in Data.ts for cleaner type inference and updates TSchema error handling test.
22 changes: 22 additions & 0 deletions packages/evolution/docs/modules/core/CBOR.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ parent: Modules
<h2 class="text-delta">Table of contents</h2>

- [constants](#constants)
- [AIKEN_DEFAULT_OPTIONS](#aiken_default_options)
- [CANONICAL_OPTIONS](#canonical_options)
- [CBOR_ADDITIONAL_INFO](#cbor_additional_info)
- [CBOR_MAJOR_TYPE](#cbor_major_type)
Expand Down Expand Up @@ -64,6 +65,25 @@ parent: Modules

# constants

## AIKEN_DEFAULT_OPTIONS

Aiken-compatible CBOR encoding options

Matches the encoding used by Aiken's cbor.serialise():

- Indefinite-length arrays (9f...ff)
- Maps encoded as arrays of pairs (not CBOR maps)
- Strings as bytearrays (major type 2, not 3)
- Constructor tags: 121-127 for indices 0-6, then 1280+ for 7+

**Signature**

```ts
export declare const AIKEN_DEFAULT_OPTIONS: CodecOptions
```

Added in v2.0.0

## CANONICAL_OPTIONS

Canonical CBOR encoding options (RFC 8949 Section 4.2.1)
Expand Down Expand Up @@ -239,6 +259,7 @@ export type CodecOptions =
| {
readonly mode: "canonical"
readonly mapsAsObjects?: boolean
readonly encodeMapAsPairs?: boolean
}
| {
readonly mode: "custom"
Expand All @@ -248,6 +269,7 @@ export type CodecOptions =
readonly sortMapKeys: boolean
readonly useMinimalEncoding: boolean
readonly mapsAsObjects?: boolean
readonly encodeMapAsPairs?: boolean
}
```

Expand Down
37 changes: 18 additions & 19 deletions packages/evolution/docs/modules/core/Data.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,6 @@ parent: Modules
- [int](#int)
- [list](#list)
- [map](#map)
- [either](#either)
- [Either (namespace)](#either-namespace)
- [equality](#equality)
- [equals](#equals)
- [hash](#hash)
Expand Down Expand Up @@ -71,6 +69,7 @@ parent: Modules
- [utils](#utils)
- [ByteArray (type alias)](#bytearray-type-alias)
- [CDDLSchema](#cddlschema)
- [DataSchema (interface)](#dataschema-interface)
- [Int (type alias)](#int-type-alias)

---
Expand All @@ -88,12 +87,12 @@ export declare const withSchema: <A, I extends Data>(
schema: Schema.Schema<A, I>,
options?: CBOR.CodecOptions
) => {
toData: (input: A) => I
fromData: (input: I) => A
toCBORHex: (input: A, options?: CBOR.CodecOptions) => string
toCBORBytes: (input: A, options?: CBOR.CodecOptions) => Uint8Array
fromCBORHex: (hex: string, options?: CBOR.CodecOptions) => A
fromCBORBytes: (bytes: Uint8Array, options?: CBOR.CodecOptions) => A
toData: (a: A, overrideOptions?: ParseOptions) => I
fromData: (i: I, overrideOptions?: ParseOptions) => A
toCBORHex: (a: A, overrideOptions?: ParseOptions) => string
toCBORBytes: (a: A, overrideOptions?: ParseOptions) => any
fromCBORHex: (i: string, overrideOptions?: ParseOptions) => A
fromCBORBytes: (i: any, overrideOptions?: ParseOptions) => A
}
```

Expand Down Expand Up @@ -175,14 +174,6 @@ export declare const map: (entries: Array<[key: Data, value: Data]>) => Map

Added in v2.0.0

# either

## Either (namespace)

Either-based variants for functions that can fail.

Added in v2.0.0

# equality

## equals
Expand Down Expand Up @@ -525,7 +516,7 @@ Combined schema for PlutusData type with proper recursion
**Signature**

```ts
export declare const DataSchema: Schema.Schema<Data, DataEncoded, never>
export declare const DataSchema: DataSchema
```

Added in v2.0.0
Expand Down Expand Up @@ -735,7 +726,7 @@ Encode PlutusData to CBOR bytes
**Signature**

```ts
export declare const toCBORBytes: (input: Data, options?: CBOR.CodecOptions) => Uint8Array
export declare const toCBORBytes: (data: Data, options?: CBOR.CodecOptions) => any
```

Added in v2.0.0
Expand All @@ -747,7 +738,7 @@ Encode PlutusData to CBOR hex string
**Signature**

```ts
export declare const toCBORHex: (input: Data, options?: CBOR.CodecOptions) => string
export declare const toCBORHex: (data: Data, options?: CBOR.CodecOptions) => string
```

Added in v2.0.0
Expand Down Expand Up @@ -808,6 +799,14 @@ export type ByteArray = typeof ByteArray.Type
export declare const CDDLSchema: Schema.Schema<CBOR.CBOR, CBOR.CBOR, never>
```

## DataSchema (interface)

**Signature**

```ts
export interface DataSchema extends Schema.SchemaClass<Data, DataEncoded> {}
```

## Int (type alias)

**Signature**
Expand Down
73 changes: 71 additions & 2 deletions packages/evolution/docs/modules/core/TSchema.ts.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ parent: Modules

- [combinators](#combinators)
- [equivalence](#equivalence)
- [constructors](#constructors)
- [TaggedStruct](#taggedstruct)
- [Variant](#variant)
- [schemas](#schemas)
- [ByteArray](#bytearray)
- [Integer](#integer)
Expand Down Expand Up @@ -62,6 +65,48 @@ export declare const equivalence: <A, I, R>(schema: Schema.Schema<A, I, R>) => E

Added in v2.0.0

# constructors

## TaggedStruct

Creates a tagged struct - a shortcut for creating a Struct with a Literal tag field.

This is a convenience helper that makes it easy to create structs with discriminator fields,
commonly used in discriminated unions.

**Signature**

```ts
export declare const TaggedStruct: <
TagValue extends string,
Fields extends Schema.Struct.Fields,
TagField extends string = "_tag"
>(
tagValue: TagValue,
fields: Fields,
options?: StructOptions & { tagField?: TagField }
) => Struct<{ [K in TagField]: OneLiteral<TagValue> } & Fields>
```

Added in v2.0.0

## Variant

Creates a variant (tagged union) schema for Aiken-style enum types.

This is a convenience helper that creates properly discriminated TypeScript types
while maintaining single-level CBOR encoding compatible with Aiken.

**Signature**

```ts
export declare const Variant: <Variants extends Record<string, Schema.Struct.Fields>>(
variants: Variants
) => Schema.Schema<VariantType<Variants>, Data.Data, never>
```

Added in v2.0.0

# schemas

## ByteArray
Expand Down Expand Up @@ -289,7 +334,31 @@ export interface StructOptions {
*
* Default: true when index is specified, false otherwise
*/
flat?: boolean
flatInUnion?: boolean
/**
* When used as a field in a parent Struct, controls whether this Struct's fields
* should be spread (merged) into the parent's field array.
* - true: Inner Struct fields are merged directly into parent
* - false: Inner Struct is kept as a nested Constr
*
* Default: false
*
* Note: This only applies when the Struct is a field value, not when used in Union.
*/
flatFields?: boolean
/**
* Name of a field to treat as a discriminant tag (e.g., "_tag", "type").
*
* Auto-detection: Fields named "_tag", "type", "kind", or "variant" containing
* Literal values are automatically stripped from CBOR encoding and injected during decoding.
*
* This option allows you to:
* - Explicitly specify a custom tag field name
* - Disable auto-detection with `tagField: false`
*
* Default: auto-detect from KNOWN_TAG_FIELDS
*/
tagField?: string | false
}
```

Expand Down Expand Up @@ -362,7 +431,7 @@ Added in v2.0.0
export interface Union<Members extends ReadonlyArray<Schema.Schema.Any>>
extends Schema.transformOrFail<
Schema.SchemaClass<Data.Constr, Data.Constr, never>,
Schema.SchemaClass<Schema.Schema.Type<[...Members][number]>, Schema.Schema.Type<[...Members][number]>, never>,
Schema.SchemaClass<Schema.Schema.Type<Members[number]>, Schema.Schema.Type<Members[number]>, never>,
never
> {}
```
Expand Down
32 changes: 32 additions & 0 deletions packages/evolution/src/core/CBOR.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export type CodecOptions =
| {
readonly mode: "canonical"
readonly mapsAsObjects?: boolean
readonly encodeMapAsPairs?: boolean
}
| {
readonly mode: "custom"
Expand All @@ -76,6 +77,7 @@ export type CodecOptions =
readonly sortMapKeys: boolean
readonly useMinimalEncoding: boolean
readonly mapsAsObjects?: boolean
readonly encodeMapAsPairs?: boolean
}

/**
Expand Down Expand Up @@ -120,6 +122,29 @@ export const CML_DATA_DEFAULT_OPTIONS: CodecOptions = {
mapsAsObjects: false
} as const

/**
* Aiken-compatible CBOR encoding options
*
* Matches the encoding used by Aiken's cbor.serialise():
* - Indefinite-length arrays (9f...ff)
* - Maps encoded as arrays of pairs (not CBOR maps)
* - Strings as bytearrays (major type 2, not 3)
* - Constructor tags: 121-127 for indices 0-6, then 1280+ for 7+
*
* @since 2.0.0
* @category constants
*/
export const AIKEN_DEFAULT_OPTIONS: CodecOptions = {
mode: "custom",
useIndefiniteArrays: true,
useIndefiniteMaps: true,
useDefiniteForEmpty: false,
sortMapKeys: false,
useMinimalEncoding: true,
mapsAsObjects: false,
encodeMapAsPairs: true
} as const

/**
* CBOR encoding options that return objects instead of Maps for Schema.Struct compatibility
*
Expand Down Expand Up @@ -877,6 +902,13 @@ const encodeMapEntriesSync = (pairs: Array<[CBOR, CBOR]>, options: CodecOptions)
const useMinimal = options.mode === "canonical" || (options.mode === "custom" && options.useMinimalEncoding)
const sortKeys = options.mode === "canonical" || (options.mode === "custom" && options.sortMapKeys)
const useIndefinite = options.mode === "custom" && options.useIndefiniteMaps && length > 0
const encodeAsPairs = options.encodeMapAsPairs === true

// If encoding as array of pairs (Aiken/Plutus style), delegate to array encoding
if (encodeAsPairs) {
const pairArrays = pairs.map(([k, v]) => [k, v] as CBOR)
return encodeArraySync(pairArrays, options)
}

// Fast path for empty maps
if (length === 0) {
Expand Down
Loading