Guides

Utilities Cookbook

Task-oriented recipes for the Hydra SDK utility namespaces — datums, redeemers, time, policies, providers, and more.

This cookbook collects task-oriented recipes for the Hydra SDK utility namespaces, distilled from the real nodejs-playground/src implementations. Each recipe is a small, runnable snippet: a problem and the shortest code that solves it. Every namespace shown here is exported from @hydra-sdk/core. For full function signatures and parameter tables, see /api/utilities.

Building datums

Construct Plutus data with the DatumUtils encoders. Start with the primitives, then compose them into structured, nested datums.

import { DatumUtils } from '@hydra-sdk/core'

// Simple datum types
const intDatum = DatumUtils.mkInt(42)
const bytesDatum = DatumUtils.mkBytes('deadbeef')
const listDatum = DatumUtils.mkList([intDatum, bytesDatum])

// Constructor datum
const constrDatum = DatumUtils.mkConstr(0, [intDatum, bytesDatum])

// Map datum
const mapDatum = DatumUtils.mkMap([
  [DatumUtils.mkBytes('key1'), intDatum],
  [DatumUtils.mkBytes('key2'), bytesDatum]
])

Complex, nested datum

The production pattern below builds a deeply nested constructor with an inner map-of-maps. This is the canonical copy of this recipe.

This buildDatum block is also used in the Mint and Burn Tokens guide, which links back here as the canonical source. Keep the two in sync.
import { DatumUtils, ParserUtils } from '@hydra-sdk/core'
import { CardanoWASM } from '@hydra-sdk/cardano-wasm'

// Production datum builder pattern
const buildDatum = (key: string, l1Vkh: string, l2Vkh: string, amount: string) => {
  const bKey = DatumUtils.mkBytes(key)
  const cL1Vkh = DatumUtils.mkConstr(0, [DatumUtils.mkBytes(l1Vkh)])
  const cL2Vkh = DatumUtils.mkConstr(0, [DatumUtils.mkBytes(l2Vkh)])

  const constrKey = DatumUtils.mkConstr(0, [bKey, cL1Vkh, cL2Vkh])
  const wrap1 = DatumUtils.mkConstr(0, [constrKey])

  // Nested map: { "" => { "" => amount } }
  const emptyBytes = DatumUtils.mkBytes('')
  const mapVal = CardanoWASM.PlutusMapValues.new()
  mapVal.add(DatumUtils.mkInt(amount))
  const innerMap = DatumUtils.mkMap([[emptyBytes, mapVal]])

  const outerMapVal = CardanoWASM.PlutusMapValues.new()
  outerMapVal.add(innerMap)
  const outerMap = DatumUtils.mkMap([[emptyBytes, outerMapVal]])

  return DatumUtils.mkConstr(0, [wrap1, outerMap])
}

// Usage
const datum = buildDatum(
  'ee91e90e791e4cd983d1b1f331d1e8eb',
  '326cd6bff6114c4d14ebf2385883aac43c4e64476e6a47314f9b2003',
  'f602ad4b16ec2e1a96989dc140eacf546359695cfece8510c8d1c0ac',
  '4000000'
)

New datum encoders

New in v1.4.0. Convenience encoders for lists, booleans, options, output references, and addresses.
import { DatumUtils, NETWORK_ID } from '@hydra-sdk/core'

// Lists
const list = DatumUtils.mkList([DatumUtils.mkInt(1), DatumUtils.mkInt(2)])
const bytesList = DatumUtils.mkBytesList(['deadbeef', 'cafe'])
const intList = DatumUtils.mkIntList([1, 2, 3])

// Booleans: False = Constr(0, []), True = Constr(1, [])
const flag = DatumUtils.mkBool(true)

// Option: Some = Constr(0, [v]), None = Constr(1, [])
const some = DatumUtils.mkOption(DatumUtils.mkInt(42))
const none = DatumUtils.mkOption()

// Output reference: Constr(0, [Bytes(txHash), Int(index)])
const outRef = DatumUtils.mkOutputRef({ txHash: 'abc123...', index: 0 })

// Address <-> PlutusData
const addrDatum = DatumUtils.mkAddress('addr_test1...')
const bech32 = DatumUtils.parseAddress(addrDatum, NETWORK_ID.PREPROD)

NFT metadata datum

Build a CIP-style metadata datum by hex-encoding every key and value.

import { DatumUtils, ParserUtils } from '@hydra-sdk/core'

const createNFTDatum = (name: string, image: string, attributes: Record<string, string>) => {
  const attributeEntries = Object.entries(attributes).map(([key, value]) => [
    DatumUtils.mkBytes(ParserUtils.stringToHex(key)),
    DatumUtils.mkBytes(ParserUtils.stringToHex(value))
  ])

  return DatumUtils.mkMap([
    [DatumUtils.mkBytes(ParserUtils.stringToHex("name")), 
     DatumUtils.mkBytes(ParserUtils.stringToHex(name))],
    [DatumUtils.mkBytes(ParserUtils.stringToHex("image")), 
     DatumUtils.mkBytes(ParserUtils.stringToHex(image))],
    [DatumUtils.mkBytes(ParserUtils.stringToHex("attributes")), 
     DatumUtils.mkMap(attributeEntries)]
  ])
}

// Usage
const nftDatum = createNFTDatum(
  "CryptoPunk #1234",
  "ipfs://QmYourHashHere",
  { "type": "Alien", "accessory": "3D Glasses" }
)

Redeemers

Wrap PlutusData into a Redeemer with RedeemerUtils.

New in v1.4.0.
import { RedeemerUtils, DatumUtils } from '@hydra-sdk/core'

// Wrap PlutusData into a Redeemer (tag defaults to 'spend', index to 0)
const data = DatumUtils.mkConstr(1, [])
const redeemer = RedeemerUtils.mkRedeemer(data, { tag: 'spend', index: 0 })

// Tag-preset helpers
const spendRedeemer = RedeemerUtils.mkSpendRedeemer(data)
const mintRedeemer = RedeemerUtils.mkMintRedeemer(data)

// The "no argument" Unit redeemer: Constr(0, [])
const unitRedeemer = RedeemerUtils.mkUnitRedeemer()

// Execution units (placeholder budget — evaluate scripts for production)
const exUnits = RedeemerUtils.mkExUnits(RedeemerUtils.DEFAULT_EX_UNITS)

Time and slots

Convert between Unix time and slots for transaction validity ranges and deadlines with TimeUtils and SLOT_CONFIG_NETWORK.

import { TimeUtils, SLOT_CONFIG_NETWORK } from '@hydra-sdk/core'

// Convert Unix timestamp to slot
const currentSlot = TimeUtils.unixTimeToEnclosingSlot(
  Date.now(), 
  SLOT_CONFIG_NETWORK.PREPROD
)

// Deadline calculation (24 hours from now)
const deadline = TimeUtils.unixTimeToEnclosingSlot(
  Date.now() + (24 * 60 * 60 * 1000),
  SLOT_CONFIG_NETWORK.PREPROD
)

// Convert slot back to Unix timestamp
const readableTime = TimeUtils.slotToBeginUnixTime(
  currentSlot, 
  SLOT_CONFIG_NETWORK.PREPROD
)

console.log('Current slot:', currentSlot)
console.log('Deadline slot:', deadline)
console.log('Readable time:', new Date(readableTime))

Vesting schedule datum

Combine TimeUtils with DatumUtils to encode a start/end slot window.

import { TimeUtils, DatumUtils, SLOT_CONFIG_NETWORK } from '@hydra-sdk/core'

const createVestingDatum = (
  beneficiary: string,
  totalAmount: bigint,
  vestingMonths: number
) => {
  const slotConfig = SLOT_CONFIG_NETWORK.PREPROD
  const startTime = Date.now() + (30 * 24 * 60 * 60 * 1000) // Start in 30 days
  const endTime = startTime + (vestingMonths * 30 * 24 * 60 * 60 * 1000)
  
  const startSlot = TimeUtils.unixTimeToEnclosingSlot(startTime, slotConfig)
  const endSlot = TimeUtils.unixTimeToEnclosingSlot(endTime, slotConfig)
  
  return DatumUtils.mkConstr(0, [
    DatumUtils.mkBytes(beneficiary),
    DatumUtils.mkInt(totalAmount),
    DatumUtils.mkInt(startSlot),
    DatumUtils.mkInt(endSlot)
  ])
}

Minting policies

Derive a native minting policy (and its policy ID) from a wallet address with PolicyUtils.

import { PolicyUtils, ParserUtils, Serializer } from '@hydra-sdk/core'

// Create policy from wallet address
const walletAddress = wallet.getAccount().baseAddressBech32
const scriptCborHex = PolicyUtils.buildMintingPolicyScriptFromAddress(walletAddress)
const policyId = PolicyUtils.policyIdFromNativeScript(scriptCborHex)

// Token name conversion
const assetNameHex = ParserUtils.stringToHex('MyToken')
const assetUnit = Serializer.serializeAssetUnit(policyId, assetNameHex)

console.log('Policy ID:', policyId)
console.log('Asset Unit:', assetUnit)

Data conversion

Convert strings, bytes, and asset units, and read amounts back out of a transaction.

import { ParserUtils } from '@hydra-sdk/core'

// String to hex for token names
const assetNameHex = ParserUtils.stringToHex('AniaToken')

// Bytes to hex round trip
const bytes = new Uint8Array([0xde, 0xad, 0xbe, 0xef])
const hex = ParserUtils.bytesToHex(bytes)
const backToBytes = ParserUtils.hexToBytes(hex)
console.log('Round trip successful:', bytes.every((b, i) => b === backToBytes[i]))

Asset unit serialization

Serialize a policyId + assetName into an asset unit, and deserialize it back.

import { ParserUtils, Serializer, Deserializer } from '@hydra-sdk/core'

const processAssetUnit = (policyId: string, assetName: string) => {
  // Serialize asset unit
  const assetNameHex = ParserUtils.stringToHex(assetName)
  const assetUnit = Serializer.serializeAssetUnit(policyId, assetNameHex)
  
  // Deserialize back
  const { policyId: deserializedPolicy, assetName: deserializedName } = 
    Deserializer.deserializeAssetUnit(assetUnit)
  
  return {
    original: { policyId, assetName },
    serialized: assetUnit,
    deserialized: { 
      policyId: deserializedPolicy, 
      assetName: ParserUtils.hexToString(deserializedName)
    }
  }
}

const assetData = processAssetUnit('abc123...', 'MyToken')

Read all amounts from a transaction

New in v1.4.0. deserializeAmountsFromTx sums every output's amounts, merged by unit.
import { Deserializer } from '@hydra-sdk/core'

// Sum every output's amounts, merged by unit
const amounts = Deserializer.deserializeAmountsFromTx(signedTxCbor)
console.log('Total amounts:', amounts)

Providers

Create a chain data provider for fetching UTxOs and submitting transactions. All providers expose the same .fetcher / .submitter surface.

Providers do not expose protocol parameters. Inside a Hydra Head, read them from the bridge instead: await bridge.getProtocolParameters().
import { ProviderUtils } from '@hydra-sdk/core'

// Create Blockfrost provider
const blockfrostProvider = new ProviderUtils.BlockfrostProvider({
  apiKey: process.env.BLOCKFROST_PROJECT_ID || '',
  network: 'preprod'
})

// Create Ogmios provider
const ogmiosProvider = new ProviderUtils.OgmiosProvider({
  network: 'preprod',
  apiEndpoint: 'http://localhost:1337'
})

// Fetch UTxOs for an address through the provider's fetcher
const utxos = await blockfrostProvider.fetcher.fetchAddressUTxOs(address)

// Submit a signed transaction through the provider's submitter
const txHash = await ogmiosProvider.submitter.submitTx(signedTxCbor)

Demeter provider

New in v1.4.0. DemeterProvider extends BlockfrostProvider; same .fetcher / .submitter surface.
import { ProviderUtils } from '@hydra-sdk/core'

const demeterProvider = new ProviderUtils.DemeterProvider({
  authToken: process.env.DEMETER_AUTH_TOKEN || '',
  network: 'preprod'
})

const utxos = await demeterProvider.fetcher.fetchAddressUTxOs(address)

Validation

Validate transaction outputs and addresses before building a transaction with ValidationUtils and AddressUtils.

New in v1.4.0.
import { ValidationUtils, AddressUtils } from '@hydra-sdk/core'

// Validate a transaction output shape
const okOutput = ValidationUtils.isValidTxOutput({
  address: 'addr_test1...',
  amount: [{ unit: 'lovelace', quantity: '2000000' }]
})

// Validate an address (type: 'bech32' | 'hex' | 'bytes', default 'bech32')
const okAddress = AddressUtils.isValidAddress('addr_test1...')

// Extract the payment key hash (returns null if not derivable)
const pubKeyHash = AddressUtils.getPubkeyHashFromAddress('addr_test1...')

Error handling

Utility calls can throw on malformed input. Wrap them so a failure returns null (or a fallback) instead of crashing the flow.

import { ParserUtils, DatumUtils } from '@hydra-sdk/core'

const safeConversion = (input: string, type: 'hex' | 'datum') => {
  try {
    switch (type) {
      case 'hex':
        return ParserUtils.stringToHex(input)
      case 'datum':
        return DatumUtils.mkBytes(ParserUtils.stringToHex(input))
      default:
        throw new Error('Unsupported conversion type')
    }
  } catch (error) {
    console.error(`Conversion failed for ${type}:`, error)
    return null
  }
}

// Usage with fallback
const result = safeConversion('test data', 'hex') || 'default_hex_value'
The same try/catch wrapper works for any utility call — for example wrap TimeUtils.unixTimeToEnclosingSlot(...) to guard against invalid timestamps.

See also