Skip to content

Commit 136e125

Browse files
Merge pull request #63 from IntersectMBO/feat/aiken-cbor-encoding
feat/aiken cbor encoding
2 parents fd405ed + 7bb1da3 commit 136e125

File tree

15 files changed

+2707
-213
lines changed

15 files changed

+2707
-213
lines changed

.changeset/brave-keys-dance.md

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
---
2+
"@evolution-sdk/evolution": patch
3+
---
4+
5+
Improve `Variant` type inference with `PropertyKey` constraint
6+
7+
The `Variant` helper now accepts `PropertyKey` (string | number | symbol) as variant keys instead of just strings, enabling more flexible discriminated union patterns.
8+
9+
**Before:**
10+
```typescript
11+
// Only string keys were properly typed
12+
const MyVariant = TSchema.Variant({
13+
"Success": { value: TSchema.Integer },
14+
"Error": { message: TSchema.ByteArray }
15+
})
16+
```
17+
18+
**After:**
19+
```typescript
20+
// Now supports symbols and numbers as variant keys
21+
const MyVariant = TSchema.Variant({
22+
Success: { value: TSchema.Integer },
23+
Error: { message: TSchema.ByteArray }
24+
})
25+
// Type inference is improved, especially with const assertions
26+
```
27+
28+
Replace `@ts-expect-error` with `as any` following Effect patterns
29+
30+
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.
31+
32+
Add comprehensive Cardano Address type support
33+
34+
Added full CBOR encoding support for Cardano address structures with Aiken compatibility:
35+
36+
```typescript
37+
const Credential = TSchema.Variant({
38+
VerificationKey: { hash: TSchema.ByteArray },
39+
Script: { hash: TSchema.ByteArray }
40+
})
41+
42+
const Address = TSchema.Struct({
43+
payment_credential: Credential,
44+
stake_credential: TSchema.UndefinedOr(
45+
TSchema.Variant({
46+
Inline: { credential: Credential },
47+
Pointer: {
48+
slot_number: TSchema.Integer,
49+
transaction_index: TSchema.Integer,
50+
certificate_index: TSchema.Integer
51+
}
52+
})
53+
)
54+
})
55+
56+
// Creates proper CBOR encoding matching Aiken's output
57+
const address = Data.withSchema(Address).toData({
58+
payment_credential: { VerificationKey: { hash } },
59+
stake_credential: { Inline: { credential: { VerificationKey: { stakeHash } } } }
60+
})
61+
```

.changeset/silent-forks-bow.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@evolution-sdk/evolution": patch
3+
---
4+
5+
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.

packages/evolution/docs/modules/core/CBOR.ts.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ parent: Modules
1111
<h2 class="text-delta">Table of contents</h2>
1212

1313
- [constants](#constants)
14+
- [AIKEN_DEFAULT_OPTIONS](#aiken_default_options)
1415
- [CANONICAL_OPTIONS](#canonical_options)
1516
- [CBOR_ADDITIONAL_INFO](#cbor_additional_info)
1617
- [CBOR_MAJOR_TYPE](#cbor_major_type)
@@ -64,6 +65,25 @@ parent: Modules
6465

6566
# constants
6667

68+
## AIKEN_DEFAULT_OPTIONS
69+
70+
Aiken-compatible CBOR encoding options
71+
72+
Matches the encoding used by Aiken's cbor.serialise():
73+
74+
- Indefinite-length arrays (9f...ff)
75+
- Maps encoded as arrays of pairs (not CBOR maps)
76+
- Strings as bytearrays (major type 2, not 3)
77+
- Constructor tags: 121-127 for indices 0-6, then 1280+ for 7+
78+
79+
**Signature**
80+
81+
```ts
82+
export declare const AIKEN_DEFAULT_OPTIONS: CodecOptions
83+
```
84+
85+
Added in v2.0.0
86+
6787
## CANONICAL_OPTIONS
6888
6989
Canonical CBOR encoding options (RFC 8949 Section 4.2.1)
@@ -239,6 +259,7 @@ export type CodecOptions =
239259
| {
240260
readonly mode: "canonical"
241261
readonly mapsAsObjects?: boolean
262+
readonly encodeMapAsPairs?: boolean
242263
}
243264
| {
244265
readonly mode: "custom"
@@ -248,6 +269,7 @@ export type CodecOptions =
248269
readonly sortMapKeys: boolean
249270
readonly useMinimalEncoding: boolean
250271
readonly mapsAsObjects?: boolean
272+
readonly encodeMapAsPairs?: boolean
251273
}
252274
```
253275

packages/evolution/docs/modules/core/Data.ts.md

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,6 @@ parent: Modules
2020
- [int](#int)
2121
- [list](#list)
2222
- [map](#map)
23-
- [either](#either)
24-
- [Either (namespace)](#either-namespace)
2523
- [equality](#equality)
2624
- [equals](#equals)
2725
- [hash](#hash)
@@ -71,6 +69,7 @@ parent: Modules
7169
- [utils](#utils)
7270
- [ByteArray (type alias)](#bytearray-type-alias)
7371
- [CDDLSchema](#cddlschema)
72+
- [DataSchema (interface)](#dataschema-interface)
7473
- [Int (type alias)](#int-type-alias)
7574

7675
---
@@ -88,12 +87,12 @@ export declare const withSchema: <A, I extends Data>(
8887
schema: Schema.Schema<A, I>,
8988
options?: CBOR.CodecOptions
9089
) => {
91-
toData: (input: A) => I
92-
fromData: (input: I) => A
93-
toCBORHex: (input: A, options?: CBOR.CodecOptions) => string
94-
toCBORBytes: (input: A, options?: CBOR.CodecOptions) => Uint8Array
95-
fromCBORHex: (hex: string, options?: CBOR.CodecOptions) => A
96-
fromCBORBytes: (bytes: Uint8Array, options?: CBOR.CodecOptions) => A
90+
toData: (a: A, overrideOptions?: ParseOptions) => I
91+
fromData: (i: I, overrideOptions?: ParseOptions) => A
92+
toCBORHex: (a: A, overrideOptions?: ParseOptions) => string
93+
toCBORBytes: (a: A, overrideOptions?: ParseOptions) => any
94+
fromCBORHex: (i: string, overrideOptions?: ParseOptions) => A
95+
fromCBORBytes: (i: any, overrideOptions?: ParseOptions) => A
9796
}
9897
```
9998
@@ -175,14 +174,6 @@ export declare const map: (entries: Array<[key: Data, value: Data]>) => Map
175174
176175
Added in v2.0.0
177176
178-
# either
179-
180-
## Either (namespace)
181-
182-
Either-based variants for functions that can fail.
183-
184-
Added in v2.0.0
185-
186177
# equality
187178
188179
## equals
@@ -525,7 +516,7 @@ Combined schema for PlutusData type with proper recursion
525516
**Signature**
526517

527518
```ts
528-
export declare const DataSchema: Schema.Schema<Data, DataEncoded, never>
519+
export declare const DataSchema: DataSchema
529520
```
530521

531522
Added in v2.0.0
@@ -735,7 +726,7 @@ Encode PlutusData to CBOR bytes
735726
**Signature**
736727
737728
```ts
738-
export declare const toCBORBytes: (input: Data, options?: CBOR.CodecOptions) => Uint8Array
729+
export declare const toCBORBytes: (data: Data, options?: CBOR.CodecOptions) => any
739730
```
740731
741732
Added in v2.0.0
@@ -747,7 +738,7 @@ Encode PlutusData to CBOR hex string
747738
**Signature**
748739
749740
```ts
750-
export declare const toCBORHex: (input: Data, options?: CBOR.CodecOptions) => string
741+
export declare const toCBORHex: (data: Data, options?: CBOR.CodecOptions) => string
751742
```
752743
753744
Added in v2.0.0
@@ -808,6 +799,14 @@ export type ByteArray = typeof ByteArray.Type
808799
export declare const CDDLSchema: Schema.Schema<CBOR.CBOR, CBOR.CBOR, never>
809800
```
810801
802+
## DataSchema (interface)
803+
804+
**Signature**
805+
806+
```ts
807+
export interface DataSchema extends Schema.SchemaClass<Data, DataEncoded> {}
808+
```
809+
811810
## Int (type alias)
812811

813812
**Signature**

packages/evolution/docs/modules/core/TSchema.ts.md

Lines changed: 71 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,9 @@ parent: Modules
1212

1313
- [combinators](#combinators)
1414
- [equivalence](#equivalence)
15+
- [constructors](#constructors)
16+
- [TaggedStruct](#taggedstruct)
17+
- [Variant](#variant)
1518
- [schemas](#schemas)
1619
- [ByteArray](#bytearray)
1720
- [Integer](#integer)
@@ -62,6 +65,48 @@ export declare const equivalence: <A, I, R>(schema: Schema.Schema<A, I, R>) => E
6265
6366
Added in v2.0.0
6467
68+
# constructors
69+
70+
## TaggedStruct
71+
72+
Creates a tagged struct - a shortcut for creating a Struct with a Literal tag field.
73+
74+
This is a convenience helper that makes it easy to create structs with discriminator fields,
75+
commonly used in discriminated unions.
76+
77+
**Signature**
78+
79+
```ts
80+
export declare const TaggedStruct: <
81+
TagValue extends string,
82+
Fields extends Schema.Struct.Fields,
83+
TagField extends string = "_tag"
84+
>(
85+
tagValue: TagValue,
86+
fields: Fields,
87+
options?: StructOptions & { tagField?: TagField }
88+
) => Struct<{ [K in TagField]: OneLiteral<TagValue> } & Fields>
89+
```
90+
91+
Added in v2.0.0
92+
93+
## Variant
94+
95+
Creates a variant (tagged union) schema for Aiken-style enum types.
96+
97+
This is a convenience helper that creates properly discriminated TypeScript types
98+
while maintaining single-level CBOR encoding compatible with Aiken.
99+
100+
**Signature**
101+
102+
```ts
103+
export declare const Variant: <Variants extends Record<string, Schema.Struct.Fields>>(
104+
variants: Variants
105+
) => Schema.Schema<VariantType<Variants>, Data.Data, never>
106+
```
107+
108+
Added in v2.0.0
109+
65110
# schemas
66111
67112
## ByteArray
@@ -289,7 +334,31 @@ export interface StructOptions {
289334
*
290335
* Default: true when index is specified, false otherwise
291336
*/
292-
flat?: boolean
337+
flatInUnion?: boolean
338+
/**
339+
* When used as a field in a parent Struct, controls whether this Struct's fields
340+
* should be spread (merged) into the parent's field array.
341+
* - true: Inner Struct fields are merged directly into parent
342+
* - false: Inner Struct is kept as a nested Constr
343+
*
344+
* Default: false
345+
*
346+
* Note: This only applies when the Struct is a field value, not when used in Union.
347+
*/
348+
flatFields?: boolean
349+
/**
350+
* Name of a field to treat as a discriminant tag (e.g., "_tag", "type").
351+
*
352+
* Auto-detection: Fields named "_tag", "type", "kind", or "variant" containing
353+
* Literal values are automatically stripped from CBOR encoding and injected during decoding.
354+
*
355+
* This option allows you to:
356+
* - Explicitly specify a custom tag field name
357+
* - Disable auto-detection with `tagField: false`
358+
*
359+
* Default: auto-detect from KNOWN_TAG_FIELDS
360+
*/
361+
tagField?: string | false
293362
}
294363
```
295364

@@ -362,7 +431,7 @@ Added in v2.0.0
362431
export interface Union<Members extends ReadonlyArray<Schema.Schema.Any>>
363432
extends Schema.transformOrFail<
364433
Schema.SchemaClass<Data.Constr, Data.Constr, never>,
365-
Schema.SchemaClass<Schema.Schema.Type<[...Members][number]>, Schema.Schema.Type<[...Members][number]>, never>,
434+
Schema.SchemaClass<Schema.Schema.Type<Members[number]>, Schema.Schema.Type<Members[number]>, never>,
366435
never
367436
> {}
368437
```

packages/evolution/src/core/CBOR.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ export type CodecOptions =
6767
| {
6868
readonly mode: "canonical"
6969
readonly mapsAsObjects?: boolean
70+
readonly encodeMapAsPairs?: boolean
7071
}
7172
| {
7273
readonly mode: "custom"
@@ -76,6 +77,7 @@ export type CodecOptions =
7677
readonly sortMapKeys: boolean
7778
readonly useMinimalEncoding: boolean
7879
readonly mapsAsObjects?: boolean
80+
readonly encodeMapAsPairs?: boolean
7981
}
8082

8183
/**
@@ -120,6 +122,29 @@ export const CML_DATA_DEFAULT_OPTIONS: CodecOptions = {
120122
mapsAsObjects: false
121123
} as const
122124

125+
/**
126+
* Aiken-compatible CBOR encoding options
127+
*
128+
* Matches the encoding used by Aiken's cbor.serialise():
129+
* - Indefinite-length arrays (9f...ff)
130+
* - Maps encoded as arrays of pairs (not CBOR maps)
131+
* - Strings as bytearrays (major type 2, not 3)
132+
* - Constructor tags: 121-127 for indices 0-6, then 1280+ for 7+
133+
*
134+
* @since 2.0.0
135+
* @category constants
136+
*/
137+
export const AIKEN_DEFAULT_OPTIONS: CodecOptions = {
138+
mode: "custom",
139+
useIndefiniteArrays: true,
140+
useIndefiniteMaps: true,
141+
useDefiniteForEmpty: false,
142+
sortMapKeys: false,
143+
useMinimalEncoding: true,
144+
mapsAsObjects: false,
145+
encodeMapAsPairs: true
146+
} as const
147+
123148
/**
124149
* CBOR encoding options that return objects instead of Maps for Schema.Struct compatibility
125150
*
@@ -877,6 +902,13 @@ const encodeMapEntriesSync = (pairs: Array<[CBOR, CBOR]>, options: CodecOptions)
877902
const useMinimal = options.mode === "canonical" || (options.mode === "custom" && options.useMinimalEncoding)
878903
const sortKeys = options.mode === "canonical" || (options.mode === "custom" && options.sortMapKeys)
879904
const useIndefinite = options.mode === "custom" && options.useIndefiniteMaps && length > 0
905+
const encodeAsPairs = options.encodeMapAsPairs === true
906+
907+
// If encoding as array of pairs (Aiken/Plutus style), delegate to array encoding
908+
if (encodeAsPairs) {
909+
const pairArrays = pairs.map(([k, v]) => [k, v] as CBOR)
910+
return encodeArraySync(pairArrays, options)
911+
}
880912

881913
// Fast path for empty maps
882914
if (length === 0) {

0 commit comments

Comments
 (0)