Skip to main content

Why Branded Types Exist

TypeScript uses structural typing — two types with the same underlying representation are treated as identical by the compiler. This means a raw bigint that represents an encrypted balance ciphertext is structurally the same as one that represents a Poseidon encryption key, or a U64 token amount. Without extra guardrails, you can pass the wrong value and TypeScript will not warn you. The SDK uses branded types (also called nominal types) to prevent this class of silent bug. Every cryptographic and numeric value in the SDK carries a phantom brand that makes it distinct from every other value with the same base type — at compile time, with zero runtime overhead.

The Type Hierarchy

The SDK’s branded types form a hierarchy. A more specific type is always assignable to its parent, but a parent is never assignable to a more specific child without an explicit helper call. Integer types (base: bigint):
  • U64 — 64-bit unsigned integer (0 to 2^64 - 1). Used for token amounts and instruction parameters.
  • U128 — 128-bit unsigned integer. Used for instruction seeds and generation indices.
  • U256 — 256-bit unsigned integer. Used for Stealth Pool Note commitment preimages and random generation seeds.
Cryptographic field elements (base: bigint, extend U256):
  • Bn254FieldElement — BN254 scalar field element. Strict upper bound: the BN254 field prime.
  • Curve25519FieldElement — Curve25519 field element. Strict upper bound: 2^255 - 19.
Poseidon types (extend Bn254FieldElement):
  • PoseidonKey — Encryption key for the Poseidon cipher scheme. Derived from the user’s master seed.
Rescue Cipher types (extend Curve25519FieldElement):
  • RcPlaintext — An unencrypted balance value.
  • RcCiphertext — An encrypted balance value stored on-chain.
  • RcKey — A session encryption key derived from the Poseidon key and account nonce.
  • RcEncryptionNonce — The monotonically increasing counter (derived from generationIndex) that ensures each encryption produces a distinct ciphertext.
Protocol types (base: Uint8Array or bigint):
  • OptionalData32 — A 32-byte opaque payload stored alongside deposits and burns. Must be encrypted or hashed — never store plaintext identifiers.
  • MicroLamportsPerAcu — Priority fee in micro-lamports per Arcium Computation Unit.
Byte array types (base: Uint8Array):
  • X25519PublicKey, X25519PrivateKey, SharedSecret — 32-byte X25519 values.
  • Sized little-endian/big-endian variants (U64LeBytes, U256BeBytes, etc.) for serialisation.

The create* Helper Functions

Raw bigint or Uint8Array values you read from on-chain accounts, compute locally, or receive from external sources are unbranded. Before passing them to any SDK function that expects a branded type, run them through the corresponding create* helper. Each helper:
  1. Validates the value at runtime (range check, type check, length check).
  2. Brands the value — returns it typed as the specific branded type.
  3. Throws a descriptive error (never silently returns undefined) if validation fails.
Import helpers from @umbra-privacy/sdk/types:

Mathematics Helpers

These are the helpers you will use most often. Token amounts, offsets, and protocol-level integers all go through here.

Field Element Helpers

Rescue Cipher Helpers

The Rescue Cipher encrypts on-chain encrypted balances. Keeping plaintexts, ciphertexts, keys, and nonces as distinct types prevents the most dangerous class of encryption bug — passing a ciphertext where the decryptor expects a key.

Protocol Helpers

Limb Helper


Common Patterns

Converting a user-supplied number to U64

User-facing forms and external APIs often produce plain number or string. Always validate before passing to the SDK:

Reading on-chain data and re-branding

Account data fetched via RPC arrives as raw bigint. Validate before any cryptographic operation:

The assert* vs create* distinction

The SDK exports both families. Use create* (not assert*) in expression contexts — assert* returns void and forces a two-step pattern:

Error handling

All helpers throw a typed error on failure. Catch at the boundary where you receive external input:

Passing branded values through the SDK

Once a value is branded, you can pass it directly to any SDK function that accepts that type — no additional wrapping needed:

What the SDK Does Not Accept

You cannot pass a plain bigint where a branded type is required — the TypeScript compiler will reject the call:
This is intentional. The validation the create* helpers perform is not optional — it is the mechanism that guarantees protocol invariants hold before values reach the on-chain program.