Evolution SDK
Encoding

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 Data module

Quick Start

import * as  from "@evolution-sdk/evolution/CBOR"

// Decode hex to a typed CBOR value
const value = .("83010203")
const value: CBOR.CBOR
// 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-identical

The CBORFormat variants are:

VariantCBOR major typeEncoding detail captured
uint0 (uint)byteSize of the argument
nint1 (nint)byteSize of the argument
bytes2 (bytes)definite vs indefinite, chunk sizes
text3 (text)definite vs indefinite, chunk sizes
array4 (array)definite vs indefinite, per-child formats
map5 (map)definite vs indefinite, key insertion order
tag6 (tag)tag header width
simple7 (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:

PresetUse when
CML_DEFAULT_OPTIONS (default)General Cardano use — definite lengths, minimal integer encoding
CANONICAL_OPTIONSRFC 8949 canonical: sorted keys, minimal encoding
CML_DATA_DEFAULT_OPTIONSPlutus data with indefinite arrays/maps
AIKEN_DEFAULT_OPTIONSAiken cbor.serialise() — indefinite arrays, maps as pairs
CARDANO_NODE_DATA_OPTIONSDefinite 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

FunctionInputReturns
fromCBORHex(hex, options?)hex stringCBOR
fromCBORBytes(bytes, options?)Uint8ArrayCBOR
fromCBORHexWithFormat(hex)hex stringDecodedWithFormat<CBOR>
fromCBORBytesWithFormat(bytes)Uint8ArrayDecodedWithFormat<CBOR>

Encode

FunctionInputReturns
toCBORHex(value, options?)CBORhex string
toCBORBytes(value, options?)CBORUint8Array
toCBORHexWithFormat(value, format)CBOR + CBORFormathex string
toCBORBytesWithFormat(value, format)CBOR + CBORFormatUint8Array

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 array

Narrow 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 .()
}