Skip to main content

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

const value = CBOR.fromCBORHex("83010203")

if (CBOR.isArray(value)) {
const len = value.length // 3
}

const hex = CBOR.toCBORHex(value) // "83010203"
const bytes = CBOR.toCBORBytes(value)

Core Concepts

The CBOR Union Type

CBOR.fromCBORHex returns a CBOR value — a discriminated union over every CBOR major type:

import { CBOR } from "@evolution-sdk/evolution"

// bigint ← CBOR major 0 (uint) and major 1 (nint)
const uint: CBOR.CBOR = 42n
const nint: CBOR.CBOR = -1n

// Uint8Array ← CBOR major 2 (bytes)
const bytes: CBOR.CBOR = new Uint8Array([0xde, 0xad])

// string ← CBOR major 3 (text)
const text: CBOR.CBOR = "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 { CBOR } from "@evolution-sdk/evolution"

const raw = CBOR.fromCBORHex("a2016161026162")

if (!CBOR.isMap(raw)) throw new CBOR.CBORError({ message: "Expected map" })

const entry = raw.get(1n)

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

const hex = "1800"

// Plain path: value decoded, encoding choices discarded
const value = CBOR.fromCBORHex(hex) // 0n
const reEncoded = CBOR.toCBORHex(value) // "00" — minimal, NOT "1800"

// WithFormat path: encoding tree captured alongside value
const { format, value: v2 } = CBOR.fromCBORHexWithFormat(hex)
const preserved = CBOR.toCBORHexWithFormat(v2, format) // "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

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

const value = CBOR.fromCBORHex("a2026161016162")

const defaultHex = CBOR.toCBORHex(value)
const canonicalHex = CBOR.toCBORHex(value, CBOR.CANONICAL_OPTIONS)

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

const v: CBOR.CBOR = CBOR.fromCBORHex("01")

CBOR.isInteger(v)
CBOR.isByteArray(v)
CBOR.isArray(v)
CBOR.isMap(v)
CBOR.isRecord(v)
CBOR.isTag(v)

Structural Matching

import { CBOR } from "@evolution-sdk/evolution"

const value = CBOR.fromCBORHex("43010203")

const result = CBOR.match(value, {
integer: (n) => `int: ${n}`,
bytes: (b) => `bytes(${b.length})`,
text: (s) => `text: ${s}`,
array: (a) => `array[${a.length}]`,
map: (m) => `map{${m.size}}`,
record: (_r) => `record`,
tag: (tag, _v) => `tag(${tag})`,
boolean: (b) => `bool: ${b}`,
null: () => `null`,
undefined: () => `undefined`,
float: (f) => `float: ${f}`,
boundedBytes: (b) => `bounded(${b.length})`,
})

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

function addWalletWitnesses(txHex: string, walletWitnessHex: string): string {
return Transaction.addVKeyWitnessesHex(txHex, walletWitnessHex)
}

Inject a hand-crafted CBORFormat for controlled encoding

import { CBOR } from "@evolution-sdk/evolution"

const fmt: CBOR.CBORFormat = {
_tag: "array",
length: { tag: "indefinite" },
children: [{ _tag: "uint" }, { _tag: "uint" }],
}

const hex = CBOR.toCBORHexWithFormat([1n, 2n], fmt)
// hex = "9f0102ff" — indefinite-length array

Narrow before accessing map entries

import { CBOR } from "@evolution-sdk/evolution"

function getMapEntry(hex: string, key: bigint): CBOR.CBOR | undefined {
const v = CBOR.fromCBORHex(hex)
if (!CBOR.isMap(v)) return undefined
return v.get(key)
}
  • Data — Plutus data encoding using the CBOR layer
  • Plutus Types — Pre-built schemas for Cardano data structures