Commit to Hydra

Committing UTxOs into a Hydra Head moves assets from Cardano Layer 1 into a state channel (the Hydra Head). This unlocks fast, low-cost transactions inside the head.

Understanding commit

What is a commit?

A commit locks UTxOs on Layer 1 and makes them available inside a Hydra Head. Think of it as “depositing” into a fast lane for your transactions.

text
┌─────────────────────────────────────────┐
│         Cardano Layer 1                 │
│                                         │
│  Your UTxOs: [UTxO1, UTxO2, UTxO3]      │
│                                         │
└──────────────┬──────────────────────────┘
               │
               │ COMMIT
               │ (locked on L1, available in head)
               │
┌──────────────▼──────────────────────────┐
│         Hydra Head (Layer 2)            │
│                                         │
│  Available: [UTxO1, UTxO2, UTxO3]       │
│  Ready for fast transactions            │
│                                         │
└─────────────────────────────────────────┘

Key concepts

  • Initialization phase: Hydra Head must be in the Initializing state
  • Commit window: Time window to commit UTxOs before the head opens
  • Participant commits: Each participant commits their own UTxOs
  • Immutable after open: No further commits once the head is open (unless using incremental commit)
  • Layer 1 transaction: A commit is always an on-chain transaction

Environment setup

Requirements

Successfully connected to Hydra Node

Folder structure

plaintext
nodejs-app/
    ├── src/
    │    ├── common.ts                # Shared config and API wrapper
    │    ├── cardano-query-utxo.ts    # Query UTxOs from Cardano Layer 1
    │    ├── empty-commit.ts          # Empty commit sample (to open head)
    │    ├── partial-commit.ts        # Commit with UTxO (incremental commit)
    │    └── ...                      # Other scripts
    ├── .env                          # Env config (Blockfrost key)
    └── package.json                  # Node.js project config

Common config and API wrapper

package.json

json
{
    "name": "nodejs-app",
    "version": "1.0.0",
    "main": "index.js",
    "license": "MIT",
    "scripts": {
        "start": "tsx src/index.ts"
    },
    "dependencies": {
        "@hydra-sdk/cardano-wasm": "^0.0.5",
        "@hydra-sdk/core": "^1.1.3",
        "@hydra-sdk/transaction": "^1.1.3",
        "@hydra-sdk/bridge": "^1.1.3",
        "axios": "^1.4.0",
        "bignumber.js": "^9.1.1",
        "dotenv": "^16.3.1"
    },
    "devDependencies": {
        "tsx": "^3.12.7",
        "typescript": "^5.2.2"
    }
}

Install dependencies:

bash
pnpm install
# or
npm install

.env

plaintext
BLOCKFROST_PROVIDER_API_KEY=your_blockfrost_api_key_here

src/common.ts

typescript
import {
    AppWallet,
    Converter,
    NETWORK_ID,
    ProviderUtils,
    UTxO,
    UTxOObject
} from '@hydra-sdk/core'
import axios from 'axios'

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

// Wallet (test wallet)
export const wallet = new AppWallet({
    key: {
        type: 'mnemonic',
        words: 'your test mnemonic words here ...'.split(' ')
    },
    networkId: NETWORK_ID.PREPROD,
    fetcher: blockfrostProvider.fetcher,
    submitter: blockfrostProvider.submitter
})

export const walletAddress = wallet.getAccount().baseAddressBech32

// Hydra endpoints
export const hydraConfig = {
    httpUrl: 'http://localhost:10005',
    wsUrl: 'ws://localhost:10005'
}

export type DepositToken = [string, Record<string, number>]

// Minimal API wrapper
export class HydraApi {
    static instance = axios.create({
        baseURL: hydraConfig.httpUrl,
        headers: { 'Content-Type': 'application/json' }
    })

    static async queryAddressUTxO(address: string): Promise<UTxO[]> {
        try {
            const utxos = await this.instance.get('/snapshot/utxo')
            const utxoObj = utxos.data as Record<string, any>
            return Converter.convertUTxOObjectToUTxO(utxoObj).filter(
                u => u.output.address === address
            )
        } catch (error) {
            console.error('Error querying address UTxO:', error)
            return []
        }
    }

    static async partialDeposit(
        blueprintTxCbor: string,
        utxo: UTxOObject,
        changeAddress: string
    ) {
        try {
            const response = await this.instance.post('/commit', {
                blueprintTx: {
                    cborHex: blueprintTxCbor,
                    type: 'Tx ConwayEra',
                    description: 'Partial commit from NodeJS Playground'
                },
                utxo,
                changeAddress
            })
            return response.data as {
                cborHex: string
                description: string
                txId: string
                type: 'Tx ConwayEra'
            }
        } catch (error) {
            console.error('Error during partial deposit:', error)
            throw error
        }
    }

    static async commit(utxo: UTxOObject) {
        try {
            const response = await this.instance.post('/commit', {
                ...utxo
            })
            return response.data as {
                cborHex: string
                description: string
                txId: string
                type: 'Tx ConwayEra'
            }
        } catch (error) {
            console.error('Error during commit:', error)
            throw error
        }
    }
}
typescript
import { CardanoWASM } from '@hydra-sdk/cardano-wasm'
import { wallet } from './common'
import { Converter, Deserializer, hexToString } from '@hydra-sdk/core'
import BigNumber from 'bignumber.js'

async function main(address?: string) {
    const utxos = await wallet.queryUTxOs(
        address || wallet.getAccount().baseAddressBech32
    )
    console.log(
        '>>> UTxOs:',
        JSON.stringify(Converter.convertUTxOToUTxOObject(utxos), null, 2)
    )
    const totalLovelace = utxos.reduce(
        (a, b) =>
            a + Number(b.output.amount.find(x => x.unit === 'lovelace')?.quantity || 0),
        0
    )
    console.log(
        '>>> Total lovelace:',
        BigNumber(totalLovelace).toFormat(),
        'lovelace',
        ' => ',
        (Number(totalLovelace) / 1_000_000).toFixed(6),
        'ADA'
    )
    const totalAssets = utxos.reduce(
        (acc, utxo) => {
            utxo.output.amount.forEach(a => {
                if (a.unit !== 'lovelace') {
                    if (!acc[a.unit]) {
                        acc[a.unit] = 0
                    }
                    acc[a.unit] += Number(a.quantity)
                }
            })
            return acc
        },
        {} as Record<string, number>
    )
    console.log('>>> Total assets:', Object.keys(totalAssets).length)
    for (const [unit, quantity] of Object.entries(totalAssets)) {
        const { policyId, assetName } = Deserializer.deserializeAssetUnit(unit)
        console.log(
            `  - ${policyId}${assetName ? '.' + assetName : ''}: ${BigNumber(quantity).toFormat()} ${hexToString(assetName)}`
        )
    }
}

if (process.argv[2] && typeof process.argv[2] === 'string') {
    const address = process.argv[2]
    try {
        CardanoWASM.Address.from_bech32(address)
    } catch (error) {
        console.error('>>> address is invalid:', error)
        process.exit(1)
    }
} else {
    main()
}

Quick setup (Windows PowerShell)

powershell
# 1) Configure Blockfrost key (preprod)
$env:BLOCKFROST_PROVIDER_API_KEY = "your_blockfrost_key"

# 2) Install dependencies (if not already)
cd nodejs-app
pnpm install

# 3) Ensure Hydra node is running at http://localhost:10005 and ws://localhost:10005

# 4) Run sample scripts
npx tsx .\src\empty-commit.ts
npx tsx .\src\partial-commit.ts

Commit to start using a Hydra Head

  • Condition: Head is in the Initializing state
  • Action: Each participant commits so the head can open (empty commits are allowed)

Example - empty commit

src/empty-commit.ts

typescript
import { HydraApi, wallet } from './common'

async function main() {
    // Empty commit: send commit request without specifying concrete UTxOs
    const commitTx = await HydraApi.commit({} as any)
    if (commitTx) {
        const signedTx = await wallet.signTx(commitTx.cborHex, true)
        console.log('Signed commit tx:', signedTx)
        // Submit the signed CBOR to the Hydra node (if needed)
        const result = await HydraApi.submitCardanoTx({
            cborHex: signedTx,
            description: 'Empty commit from NodeJS Playground',
            type: 'Witnesses Tx ConwayEra'
        })
    }
}

main()

Example - commit a specific UTxO

  1. Query UTxOs from Cardano Layer 1 (if needed) src/cardano-query-utxo.ts
bash
npx tsx ./src/cardano-query-utxo.ts
json
{
    "c2e3452de098d13ae536c3fb9df599d119631d618aaa2738522aeced2d2a1ac2#0": {
        "address": "addr_test1qpxsf0x8xypuhq5k408f9kh0meyy6jv2lxgqw2fefvjlte0u06dugtmxuhhw8hschdn4q59g64q5s9z42ax6qyg7ewsqt6e548",
        "datum": null,
        "datumhash": null,
        "inlineDatum": null,
        "inlineDatumRaw": null,
        "referenceScript": null,
        "value": {
            "lovelace": 1327480,
            "e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72": {
                "4d494e": 2000000000
            },
            "fef67460342d081cb7881318b1f33b87626d1a1042b4c2acbbc0725d": {
                "7441424f": 1000000
            }
        }
    }
}
  1. Commit with a specific UTxO src/commit-with-utxo.ts
typescript
import { Converter, UTxOObject } from '@hydra-sdk/core'
import { HydraApi, wallet, walletAddress } from './common'

async function main() {
    // 1) Prepare the specific UTxO to commit
    const utxoToCommit: UTxOObject = {
        'c2e3452de098d13ae536c3fb9df599d119631d618aaa2738522aeced2d2a1ac2#0': {
            address:
                'addr_test1qpxsf0x8xypuhq5k408f9kh0meyy6jv2lxgqw2fefvjlte0u06dugtmxuhhw8hschdn4q59g64q5s9z42ax6qyg7ewsqt6e548',
            datum: null,
            datumhash: null,
            inlineDatum: null,
            inlineDatumRaw: null,
            referenceScript: null,
            value: {
                lovelace: 1327480,
                e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72: {
                    '4d494e': 2000000000
                },
                fef67460342d081cb7881318b1f33b87626d1a1042b4c2acbbc0725d: {
                    '7441424f': 1000000
                }
            }
        }
    }

    // 2) Call commit with the specific UTxO
    const result = await HydraApi.commit(utxoToCommit)

    if (result) {
        const signedTx = await wallet.signTx(result.cborHex, true)
        console.log('Signed commit tx:', signedTx)

        // 3) Submit the signed CBOR to the Hydra node
        const submitResult = await HydraApi.submitCardanoTx({
            cborHex: signedTx,
            description: 'Commit specific UTxO from NodeJS Playground',
            type: 'Witnesses Tx ConwayEra'
        })
    }
}

main()
bash
npx tsx ./src/commit-with-utxo.ts

Incremental commit (deposit more into an open head)

  • Condition: Head is in the Open state
  • Action: Any participant can commit additional UTxOs

Deposit variants for an open head:

  • Provide UTxOs only: Simple deposit, the entire UTxO is deposited into the head
  • Provide UTxOs with a blueprint transaction without outputs: Entire UTxO is deposited, and you can attach a blueprint transaction (for dApps)
  • Provide UTxOs with a blueprint transaction that has outputs but no change address: Requires a fully balanced blueprint; all outputs are deposited
  • Provide UTxOs with a blueprint transaction that has outputs and a change address: hydra-node will balance the tx; any change is returned to the provided address, while outputs are deposited

Example: Commit the entire UTxO

You can use the same HydraApi.commit() method as shown in the initial commit examples above to commit entire UTxOs during the incremental deposit phase.

Example: Commit with a blueprint transaction (no outputs)

When committing without outputs in the blueprint transaction, the entire UTxO will be deposited. This is useful when you want to attach metadata or additional information via the blueprint transaction.

Example: Commit with a blueprint transaction (has outputs, no change address)

Not recommended: Requires a fully balanced blueprint; all outputs are fully deposited. This approach is complex and error-prone. Use the method with change address instead (shown below).

Example: partial commit (with change address)

Recommended: hydra-node balances the tx and sends change back to the provided changeAddress; outputs are deposited

src/partial-commit.ts

typescript
import { Converter, Deserializer, Resolver, UTxO } from '@hydra-sdk/core'
import { HydraApi, wallet, walletAddress } from './common'
import { TxBuilder } from '@hydra-sdk/transaction'

/**
 * Partial commit example
 * 1. Make sure you have some assets in your wallet
 * 2. Run this script to create a partial commit transaction
 * 3. Sign the transaction and submit it to Hydra node
 *
 * Note: Build the blueprint transaction
 * - https://hydra.iohk.io/docs/hydra-node/tutorials/blueprint-tx/
 * - The outputs of the blueprint transaction will be used as inputs for the partial commit transaction
 *
 * Command:
 * > npx tsx src/hydra/partial-commit.ts
 */
async function main() {
    console.log('>>> walletAddress:', walletAddress)
    const l1UTxOs = await wallet.queryUTxOs(walletAddress)

    const depositLovelace = 180_000_000 // 180 ADA
    // assets to deposit
    const depositAssetUnits = [
        {
            unit: 'e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed724d494e',
            quantity: '1000000' // 1,000,000 token units
        }
    ]

    // check if asset exists in the wallet
    const hasAsset = l1UTxOs.some(utxo =>
        utxo.output.amount.some(a => depositAssetUnits.findIndex(b => b.unit === a.unit) >= 0)
    )
    if (!hasAsset) {
        throw new Error(`No asset ${depositAssetUnits.join(', ')} found in the wallet`)
    }

    const txBuilder = new TxBuilder({
        errorLogger: true,
        isHydra: true,
        params: {
            minFeeA: 0,
            minFeeB: 0
        }
    })

    // build the blueprint transaction
    const blueprintTx = await txBuilder
        .setInputs(l1UTxOs)
        .addOutput({
            address: walletAddress,
            amount: [{ unit: 'lovelace', quantity: depositLovelace.toString() }, ...depositAssetUnits]
        })
        .setFee('0')
        .complete()

    console.log('>>> blueprintTx.to_hex():', blueprintTx.to_hex())
    const txHash = Resolver.resolveTxHash(blueprintTx.to_hex())
    console.log('>>> txHash:', txHash)

    const txInputs = Deserializer.deserializeTx(blueprintTx.to_hex()).body().inputs()
    const utxoToCommit: UTxO[] = []
    for (let i = 0; i < txInputs.len(); i++) {
        const input = txInputs.get(i)
        if (input) {
            const utxo = l1UTxOs.find(
                u => u.input.txHash === input.transaction_id().to_hex() && u.input.outputIndex === input.index()
            )
            if (utxo) {
                utxoToCommit.push(utxo)
            }
        }
    }

    const partialCommitResult = await HydraApi.partialDeposit(
        blueprintTx.to_hex(),
        Converter.convertUTxOToUTxOObject(utxoToCommit),
        walletAddress
    )
    if (partialCommitResult) {
        const signedTx = await wallet.signTx(partialCommitResult.cborHex, true)
        console.log('>>> Signed partial commit tx:', { ...partialCommitResult, cborHex: signedTx })
    }
}

main()

Run the script:

bash
npx tsx ./src/partial-commit.ts

Commit a script UTxO into the head

  • Condition: Head is Initializing or Open
  • Action: A participant can commit UTxOs locked by a Plutus script

Example: To be added (work in progress)

References