HTLC & Preimage API
Hydra App settles swaps two ways: through payment channels (the default) and, since 2026-06-11, on-chain via HTLCs (Hash-Time-Locked Contracts). This page covers the two services that expose the on-chain path:
PreimageService(namespacepreimage) — register a revealed preimage and settle every leg it unlocks. This replacedNodeService.RegisterPreimage.HtlcService(namespacehtlc) — low-level on-chain HTLC operations: lock, claim, refund, verify, watch, and external-signer transaction builders.
These are advanced, low-level surfaces. Most integrations never call
HtlcServicedirectly — they opt a swap leg on-chain viaSwapRequest.settlement/ the orderbookOrderSettlementand let the node drive the HTLCs, observing progress through swap milestones andSubscribeHtlcEvents. Reach forHtlcServicewhen you run your own signer or build cross-chain flows by hand.For field-level detail always consult the authoritative schema —
htlc.proto,preimage.proto,currency.proto.
Channel vs on-chain settlement
Every swap has a sending leg (you → orderbook hub) and a receiving leg (hub → you). Each leg settles independently through either a channel or an on-chain HTLC.
- Channel (default). Requires an active channel for that currency. Instant, cheap, no on-chain footprint — the historical Hydra path.
- On-chain HTLC. Lets you trade a currency you hold on-chain, with no channel for it. Slower and costs chain fees, but needs no pre-funded channel.
You express the preference per order as an OrderSettlement (from currency.proto), signed into the order:
| Field | Type | Meaning |
|---|---|---|
sending | LegSettlement | How you send to the hub |
receiving | LegSettlement | How you receive from the hub |
route_filter | uint32 (optional) | Taker-only route-wide method bitmask: bit 0 = Channel, bit 1 = On-chain. Absent = no constraint. Must be absent on a resting maker order. |
LegSettlement values: CHANNEL (0, the default when OrderSettlement is absent), ONCHAIN (1), CHANNEL_OR_ONCHAIN (2 — the orderbook picks, preferring channel).
Rules & mechanics:
- On-chain is valid only for taker orders (
market_order/swap_order/Swap/SimpleSwap). Resting maker orders (limit orders) are channel-only. - The on-chain refund/claim addresses are not declared in the order. The node supplies them later, at the orderbook's
PrepareSwaphandshake.OrderSettlementis a pure method selector. - Once matched, per-hop on-chain details surface on the route as
SwapHop.sending_onchain/receiving_onchain, and progress arrives as simple-swap milestones. - The atomic-swap invariant is unchanged: the receiving leg's HTLC, when claimed, reveals the preimage, which then unlocks the sending leg. On-chain, that preimage becomes public on-chain when the counterparty claims.
Settle Preimage
Namespace preimage. Added 2026-06-11 — replaced node.RegisterPreimage.
Registers a revealed preimage S on a network and settles every leg it unlocks in one uniform act: it persists S durably, claims held channel hashlock payments (and arms the force-close / on-chain settlement), and — on HTLC-capable networks — claims every on-chain Locked HTLC paying this node whose hash is sha256(S). Idempotent.
Method: SettlePreimage
| Name | Type | Required | Description |
|---|---|---|---|
network | Network | YES | Target network |
payment_preimage | string | YES | Hex-encoded preimage to settle |
Response: Empty.
This is the drop-in replacement for the removed
node.RegisterPreimage— same request fields — that additionally claims on-chain HTLCs. See Node API → Register Preimage (moved).
import { PreimageServiceClient } from './proto/PreimageServiceClientPb'
import { SettlePreimageRequest } from './proto/preimage_pb'
const preimage = new PreimageServiceClient('http://localhost:5001')
const request = new SettlePreimageRequest()
request.setNetwork({ protocol: 1, id: '0a03cf40' })
request.setPaymentPreimage('9f8e7d6c...')
await preimage.settlePreimage(request, {}) // settles channel + on-chain legs
HtlcService RPCs
Namespace htlc. Works across Bitcoin (P2WSH / Taproot) and EVM (HtlcFactory contract). One-line summaries below; see htlc.proto for full request/response shapes.
Node-signed lock / claim / refund
The node signs and broadcasts, keying the refund branch to itself.
| RPC | Purpose |
|---|---|
CreateHtlc | Lock funds into a new on-chain HTLC. Returns the Htlc + the broadcast lock txid. |
BatchCreateHtlcs | Lock several HTLCs in one transaction (where the protocol supports it). |
ClaimHtlc | Claim an HTLC by revealing the preimage. |
RefundHtlc | Refund an expired HTLC back to the sender. |
External-signer transaction builders
The node builds an unsigned transaction (calldata / PSBT); you own signing and broadcast. Unlike CreateHtlc, these key the refund branch to a refund_address you supply.
| RPC | Purpose |
|---|---|
CreateHtlcLockTx | Build an unsigned HTLC lock transaction for an external signer. |
CreateHtlcClaimTx | Build an unsigned HTLC claim transaction. |
CreateHtlcRefundTx | Build an unsigned HTLC refund transaction. |
CreateHtlcSettlementTx | Build an unsigned settlement tx spending an HTLC to a fixed output, for pre-signing. UTXO-model protocols only (account-model settle permissionlessly → FAILED_PRECONDITION). |
BroadcastHtlcSettlement | Broadcast a user-pre-signed settlement tx, injecting the preimage for a claim. UTXO-model only. |
Each builder returns an HtlcTransactionRequest = an unsigned TransactionRequest plus the pre-computed Htlc(s) it creates or spends.
Verify & read
| RPC | Purpose |
|---|---|
VerifyHtlc | Verify an on-chain HTLC matches an HtlcExpectation (recipient, hash, min amount, min timelock, refund key, required lock_type), seed the node's record, return the Htlc. |
BatchVerifyHtlcs | Verify many HTLCs at once. |
GetChainHtlc | Raw chain read of an HTLC by its lock transaction — no expectation checks, no persistence. Returns no HTLC when the chain can't produce it (unknown lock tx; unspent/hidden Bitcoin leaf). |
VerifyHtlcByLockTxid | Locate the HTLC output inside an attested funding tx, verify it, seed the record, return the Htlc (with its chain-native id). Entry point for an externally-funded HTLC whose outpoint the funder can't know in advance. UTXO-model only. |
DeriveHtlcAddress | The chain address the given HTLC parameters commit to — the address an external wallet pays to fund the HTLC. UTXO-model only. |
Keys & watching {#watch--unwatch}
| RPC | Purpose |
|---|---|
GetHtlcPubkey | The node's canonical HTLC public key on a network — the key its locks refund to and that counterparties lock to. |
GetUniqueHtlcPubkey | Reserve a fresh, unique HTLC public key (UTXO: never-reused per call; account-model: the single stable key). |
WatchHtlc | Watch an externally-locked HTLC the node didn't create, so its transitions arrive on SubscribeHtlcEvents. Fire-and-forget; optional abandon_at_unix_seconds and confirmation_depth. |
UnwatchHtlc | Stop observing an HTLC. Idempotent. |
Key types
The Htlc type
The unified on-chain HTLC snapshot (replaced the old HtlcState / HtlcStatus-enum). An HTLC is single-output — one recipient, one amount; a multi-recipient lock is several Htlcs carried by one transaction.
| Field | Type | Description |
|---|---|---|
htlc_id | string | Chain-native id — EVM: 32-byte keccak commitment (hex); Bitcoin: "txid:vout" |
asset_id | string | Locked asset's canonical id; the zero address for the native asset |
recipient | string | Claim key/address funds release to on claim |
amount | DecimalString | Locked amount, in the asset's display unit (e.g. BTC, ETH) |
payment_hash | bytes | 32-byte SHA-256 hashlock |
refund_address | string | Refund/sender key the timeout branch pays |
expiry | Timelock | Absolute timelock after which the refund branch opens |
lock_txid | string | Id of the transaction that locked this HTLC |
lock_depth | LedgerDepth (optional) | Depth the lock confirmed at; unset while unconfirmed / in mempool |
status | HtlcStatus | Lifecycle: Locked / Claimed / Refunded |
Two migrations to note (2026-06-11): amounts are now in the asset's display unit (the node scales to base units by decimals), and timelocks are absolute (
Timelock— Unix-seconds / block-height / slot / …), not "N blocks from now". The lock RPCs taketimelock_unix_seconds, nottimelock_blocks.
HtlcStatus
A oneof of:
Locked {}— on-chain. Distinguish confirmed (presentHtlc.lock_depth) from unconfirmed (absent).Claimed { claim_txid, claim_depth?, preimage }— recipient claimed; the 32-bytepreimage(the swap secret) is revealed.Refunded { refund_txid, refund_depth? }— refunded to the sender after timelock expiry.
Timelock and LedgerDepth
Both are protocol-native { value: uint64, kind: enum } pairs — kind fixes how value is read, so one shape spans every chain:
TimelockKind:UNIX_TIMESTAMP·BLOCK_HEIGHT·SLOT·SLOT_EPOCH·CHECKPOINT·ROUND·LEDGER_SEQUENCELedgerDepthKind:BLOCK_HEIGHT·SLOT·CHECKPOINT·ROUND·LEDGER_SEQUENCE
lock_type
Added 2026-07-11. A canonical protocol-defined script-kind token (e.g. Bitcoin "taproot" / "p2wsh") on the lock-building RPCs; empty selects the node's signer-derived default. On HtlcExpectation.lock_type it is a required-match pin — a receiving party sets it to the type its signer can settle, and any other type fails verification.
Route-level on-chain fields
On a matched order's route (SwapHop, from orderbook.proto):
sending_onchain(OnchainSendSettlement) — this hop's sending leg locks an HTLC the counterparty claims:counterparty_htlc_pubkey,timelock(TimelockSpec),confirmation_depth. Unset = channel.receiving_onchain(OnchainRecvSettlement) — the counterparty locks; this side verifies + claims:confirmation_depth. The counterparty's key / htlc_id / timelock arrive at runtime.
See also
- Swap API — opting a swap leg on-chain via
SwapRequest.settlement - Orderbook API → On-chain settlement — the
OrderSettlementfield reference - Events API → Subscribe HTLC Events — on-chain HTLC lifecycle stream
- Events API → On-chain HTLC Updates — swap milestones for on-chain legs
/proto/htlc.proto,/proto/preimage.proto,/proto/currency.proto— authoritative schemas