CBOR
Low-level CBOR encoding, decoding, and byte-identical re-encoding
CBOR
Low-level CBOR encode/decode with optional byte-identical re-encoding via the WithFormat API.
Overview
CBOR is the lowest-level encoding layer in the SDK. It decodes raw CBOR bytes into a typed CBOR union value, and encodes that value back to bytes using configurable options.
Most application code should use the module-specific APIs (Transaction.fromCBORHex, TransactionBody.fromCBORHex, etc.) rather than this module directly. Use CBOR when you need to inspect or manipulate raw CBOR structures, implement a custom codec, or re-encode bytes with exact encoding preservation.
When NOT to use this module directly:
- Parsing a full transaction → use
Transaction.fromCBORHex - Encoding a domain type → use the module's own
toCBORHex/toCBORBytes - Working with Plutus data → use the
Datamodule
Quick Start
import * as from "@evolution-sdk/evolution/CBOR"
// Decode hex to a typed CBOR value
const value = .("83010203")
// Inspect the type before narrowing
if (.()) {
const = . // 3
}
// Re-encode to hex (canonical by default)
const = .() // "83010203"
// Or to bytes
const = .()Core Concepts
The CBOR Union Type
CBOR.fromCBORHex returns a CBOR value — a discriminated union over every CBOR major type:
import * as from "@evolution-sdk/evolution/CBOR"
// bigint ← CBOR major 0 (uint) and major 1 (nint)
const : . = 42n
const : . = -1n
// Uint8Array ← CBOR major 2 (bytes)
const : . = new ([0xde, 0xad])
// string ← CBOR major 3 (text)
const : . = "hello"
// ReadonlyArray<CBOR> ← CBOR major 4 (array)
// ReadonlyMap<CBOR, CBOR> ← CBOR major 5 (map)
// { _tag: "Tag"; tag: number; value: CBOR } ← CBOR major 6 (tag)
// boolean | null | undefined ← CBOR major 7 (simple)Use the type guards to narrow before accessing:
import * as from "@evolution-sdk/evolution/CBOR"
const = .("a2016161026162") // { 1 → "a", 2 → "b" }
if (!.()) throw new .({ : "Expected map" })
// raw is now ReadonlyMap<CBOR.CBOR, CBOR.CBOR>
const = .(1n) // "a"Available guards: isInteger, isByteArray, isArray, isMap, isRecord, isTag.
CBORFormat — The Encoding Tree
Standard CBOR decode throws away encoding choices: whether an integer used 1 or 4 bytes, whether a map was definite or indefinite, what order keys appeared in. This is fine for domain types that own their encoding. It is a problem for relay services that must hand back byte-identical transactions.
CBORFormat is a discriminated union (8 variants) that captures the complete encoding tree for every CBOR node. fromCBORHexWithFormat / fromCBORBytesWithFormat decode and capture it simultaneously. toCBORHexWithFormat / toCBORBytesWithFormat replay the captured tree exactly.
import * as from "@evolution-sdk/evolution/CBOR"
// "1800" = integer 0 encoded as a 1-byte uint (non-canonical; minimal is "00")
const = "1800"
// Plain path: value decoded, encoding choices discarded
const = .() // 0n
const = .() // "00" — minimal, NOT "1800"
// WithFormat path: encoding tree captured alongside value
const { , : } = .()
const = .(, ) // "1800" — byte-identicalThe CBORFormat variants are:
| Variant | CBOR major type | Encoding detail captured |
|---|---|---|
uint | 0 (uint) | byteSize of the argument |
nint | 1 (nint) | byteSize of the argument |
bytes | 2 (bytes) | definite vs indefinite, chunk sizes |
text | 3 (text) | definite vs indefinite, chunk sizes |
array | 4 (array) | definite vs indefinite, per-child formats |
map | 5 (map) | definite vs indefinite, key insertion order |
tag | 6 (tag) | tag header width |
simple | 7 (simple) | (no choices to capture) |
CodecOptions Presets
The plain toCBORHex/toCBORBytes APIs accept a CodecOptions argument that controls how values are encoded. Pre-built presets cover the most common Cardano tool conventions:
| Preset | Use when |
|---|---|
CML_DEFAULT_OPTIONS (default) | General Cardano use — definite lengths, minimal integer encoding |
CANONICAL_OPTIONS | RFC 8949 canonical: sorted keys, minimal encoding |
CML_DATA_DEFAULT_OPTIONS | Plutus data with indefinite arrays/maps |
AIKEN_DEFAULT_OPTIONS | Aiken cbor.serialise() — indefinite arrays, maps as pairs |
CARDANO_NODE_DATA_OPTIONS | Definite Plutus data (tooling compatibility) |
import * as from "@evolution-sdk/evolution/CBOR"
const = .("a2026161016162") // map with non-sorted keys
// Default (CML): preserve JS Map insertion order
const = .()
// Canonical: sort keys, minimal integer sizes
const = .(, .)Reference
Decode
| Function | Input | Returns |
|---|---|---|
fromCBORHex(hex, options?) | hex string | CBOR |
fromCBORBytes(bytes, options?) | Uint8Array | CBOR |
fromCBORHexWithFormat(hex) | hex string | DecodedWithFormat<CBOR> |
fromCBORBytesWithFormat(bytes) | Uint8Array | DecodedWithFormat<CBOR> |
Encode
| Function | Input | Returns |
|---|---|---|
toCBORHex(value, options?) | CBOR | hex string |
toCBORBytes(value, options?) | CBOR | Uint8Array |
toCBORHexWithFormat(value, format) | CBOR + CBORFormat | hex string |
toCBORBytesWithFormat(value, format) | CBOR + CBORFormat | Uint8Array |
Type Guards
import * as from "@evolution-sdk/evolution/CBOR"
const : . = .("01")
.() // v is bigint
.() // v is Uint8Array
.() // v is ReadonlyArray<CBOR.CBOR>
.() // v is ReadonlyMap<CBOR.CBOR, CBOR.CBOR>
.() // v is Record<string | number, CBOR.CBOR>
.() // v is { _tag: "Tag"; tag: number; value: CBOR.CBOR }Structural Matching
match provides exhaustive pattern matching over a CBOR value, analogous to a switch on the full union:
import * as from "@evolution-sdk/evolution/CBOR"
const = .("43010203") // bytes [01, 02, 03]
const = .(, {
: () => `int: ${}`,
: () => `bytes(${.})`,
: () => `text: ${}`,
: () => `array[${.}]`,
: () => `map{${.}}`,
: () => `record`,
: (, ) => `tag(${})`,
: () => `bool: ${}`,
: () => `null`,
: () => `undefined`,
: () => `float: ${}`,
: () => `bounded(${.})`,
})
// result = "bytes(3)"Best Practices
Use WithFormat in any relay or signing service
If your code receives a Transaction CBOR hex from a client, adds witnesses, and returns the result, use WithFormat at the transaction level to guarantee the body bytes — and thus the txId — are never altered:
import * as from "@evolution-sdk/evolution/Transaction"
function (: string, : string): string {
// Byte-level splice: body bytes untouched, txId stable
return .(, )
}For cases where you need to inspect the transaction before re-encoding, use the WithFormat round-trip:
import * as from "@evolution-sdk/evolution/Transaction"
function (: string): string {
const { , : } = .()
// Inspect tx.body, tx.witnessSet, etc. — no mutation
const = ..
// Re-encode with the captured format: body bytes byte-identical
return .(, )
}Inject a hand-crafted CBORFormat for controlled encoding
When you need a specific encoding shape that differs from the defaults — e.g. forcing an indefinite-length array — build the CBORFormat explicitly:
import * as from "@evolution-sdk/evolution/CBOR"
const : . = {
: "array",
: { : "indefinite" },
: [{ : "uint" }, { : "uint" }],
}
const = .([1n, 2n], )
// hex = "9f0102ff" — indefinite-length arrayNarrow before accessing map entries
fromCBORHex returns CBOR, not a narrowed type. Always check before indexing:
import * as from "@evolution-sdk/evolution/CBOR"
function (: string, : bigint): . | undefined {
const = .()
if (!.()) return
return .()
}