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.
┌─────────────────────────────────────────┐
│ 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
Initializingstate - 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
- Node.js 18+
- pnpm (or npm)
- A running Hydra node
- API http://localhost:10005 (HTTP) and ws://localhost:10005 (WebSocket)
- Blockfrost API key (preprod): Register here
- Testnet ADA faucet: Get testnet ADA
- Guides:
- Run a local Hydra Node
- Or orchestrate multiple Hydra Nodes via our Hydra Hexcore
Successfully connected to Hydra NodeFolder structure
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
{
"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:
pnpm install
# or
npm install
.env
BLOCKFROST_PROVIDER_API_KEY=your_blockfrost_api_key_here
src/common.ts
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
}
}
}
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)
# 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
Initializingstate - Action: Each participant commits so the head can open (empty commits are allowed)
Example - empty commit
src/empty-commit.ts
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
- Query UTxOs from Cardano Layer 1 (if needed)
src/cardano-query-utxo.ts
npx tsx ./src/cardano-query-utxo.ts
{
"c2e3452de098d13ae536c3fb9df599d119631d618aaa2738522aeced2d2a1ac2#0": {
"address": "addr_test1qpxsf0x8xypuhq5k408f9kh0meyy6jv2lxgqw2fefvjlte0u06dugtmxuhhw8hschdn4q59g64q5s9z42ax6qyg7ewsqt6e548",
"datum": null,
"datumhash": null,
"inlineDatum": null,
"inlineDatumRaw": null,
"referenceScript": null,
"value": {
"lovelace": 1327480,
"e16c2dc8ae937e8d3790c7fd7168d7b994621ba14ca11415f39fed72": {
"4d494e": 2000000000
},
"fef67460342d081cb7881318b1f33b87626d1a1042b4c2acbbc0725d": {
"7441424f": 1000000
}
}
}
}
- Commit with a specific UTxO
src/commit-with-utxo.ts
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()
npx tsx ./src/commit-with-utxo.ts
Incremental commit (deposit more into an open head)
- Condition: Head is in the
Openstate - 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
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:
npx tsx ./src/partial-commit.ts
Commit a script UTxO into the head
- Condition: Head is
InitializingorOpen - Action: A participant can commit UTxOs locked by a Plutus script
Example: To be added (work in progress)
