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 {  } from "@evolution-sdk/evolution"

// Decode hex to a typed CBOR value
const value = .("83010203")
const value: 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 {  } from "@evolution-sdk/evolution"

// 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 {  } from "@evolution-sdk/evolution"

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 {  } from "@evolution-sdk/evolution"

// "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_OPTIONSCanonical CBOR as CIP-21 specifies: map keys length-first (RFC 7049 Section 3.9), minimal encoding
HW_CLI_OPTIONSCANONICAL_OPTIONS with keyOrder: "bytewise": map keys in bytewise order (RFC 8949 Section 4.2.1), which cardano-hw-cli checks before it witnesses a transaction. TX_HW_CLI_OPTIONS applies it to a whole transaction
PLUTUS_DATA_OPTIONSPlutus data in the node layout: indefinite lists and constructor fields, definite maps. The default for Data encoding, UPLC.applyParamsToScript and UPLC.dataConstant; matches cardano-cli, Aiken cbor.serialise(), and aiken blueprint apply
CML_DATA_DEFAULT_OPTIONSDeprecated. Indefinite lists, constructor fields and maps, keys in insertion order. The Data default before PLUTUS_DATA_OPTIONS; use it only to reproduce datum bytes and hashes the SDK wrote before. It matches no tool exactly: CML PlutusData.to_cardano_node_format() also sorts map keys
CML_DATA_DEFINITE_OPTIONSMatches CML PlutusData.to_cbor_hex() on new data: definite lists and maps
AIKEN_DEFAULT_OPTIONSDeprecated alias of PLUTUS_DATA_OPTIONS
CARDANO_NODE_DATA_OPTIONSDeprecated alias of CML_DATA_DEFINITE_OPTIONS
import {  } from "@evolution-sdk/evolution"

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 {  } from "@evolution-sdk/evolution"

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 {  } from "@evolution-sdk/evolution"

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 {  } from "@evolution-sdk/evolution"

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 {  } from "@evolution-sdk/evolution"

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 {  } from "@evolution-sdk/evolution"

const : .CBORFormat = {
  : "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 {  } from "@evolution-sdk/evolution"

function (: string, : bigint): . | undefined {
  const  = .()
  if (!.()) return 
  return .()
}

On this page