Api

HTLC & Preimage API

On-chain HTLC settlement, the channel-vs-on-chain model, and preimage settlement

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 (namespace preimage) — register a revealed preimage and settle every leg it unlocks. This replaced NodeService.RegisterPreimage.
  • HtlcService (namespace htlc) — 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 HtlcService directly — they opt a swap leg on-chain via SwapRequest.settlement / the orderbook OrderSettlement and let the node drive the HTLCs, observing progress through swap milestones and SubscribeHtlcEvents. Reach for HtlcService when 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:

FieldTypeMeaning
sendingLegSettlementHow you send to the hub
receivingLegSettlementHow you receive from the hub
route_filteruint32 (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 PrepareSwap handshake. OrderSettlement is 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

NameTypeRequiredDescription
networkNetworkYESTarget network
payment_preimagestringYESHex-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.

RPCPurpose
CreateHtlcLock funds into a new on-chain HTLC. Returns the Htlc + the broadcast lock txid.
BatchCreateHtlcsLock several HTLCs in one transaction (where the protocol supports it).
ClaimHtlcClaim an HTLC by revealing the preimage.
RefundHtlcRefund 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.

RPCPurpose
CreateHtlcLockTxBuild an unsigned HTLC lock transaction for an external signer.
CreateHtlcClaimTxBuild an unsigned HTLC claim transaction.
CreateHtlcRefundTxBuild an unsigned HTLC refund transaction.
CreateHtlcSettlementTxBuild an unsigned settlement tx spending an HTLC to a fixed output, for pre-signing. UTXO-model protocols only (account-model settle permissionlessly → FAILED_PRECONDITION).
BroadcastHtlcSettlementBroadcast 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

RPCPurpose
VerifyHtlcVerify 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.
BatchVerifyHtlcsVerify many HTLCs at once.
GetChainHtlcRaw 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).
VerifyHtlcByLockTxidLocate 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.
DeriveHtlcAddressThe 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}

RPCPurpose
GetHtlcPubkeyThe node's canonical HTLC public key on a network — the key its locks refund to and that counterparties lock to.
GetUniqueHtlcPubkeyReserve a fresh, unique HTLC public key (UTXO: never-reused per call; account-model: the single stable key).
WatchHtlcWatch 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.
UnwatchHtlcStop 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.

FieldTypeDescription
htlc_idstringChain-native id — EVM: 32-byte keccak commitment (hex); Bitcoin: "txid:vout"
asset_idstringLocked asset's canonical id; the zero address for the native asset
recipientstringClaim key/address funds release to on claim
amountDecimalStringLocked amount, in the asset's display unit (e.g. BTC, ETH)
payment_hashbytes32-byte SHA-256 hashlock
refund_addressstringRefund/sender key the timeout branch pays
expiryTimelockAbsolute timelock after which the refund branch opens
lock_txidstringId of the transaction that locked this HTLC
lock_depthLedgerDepth (optional)Depth the lock confirmed at; unset while unconfirmed / in mempool
statusHtlcStatusLifecycle: 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 take timelock_unix_seconds, not timelock_blocks.

HtlcStatus

A oneof of:

  • Locked {} — on-chain. Distinguish confirmed (present Htlc.lock_depth) from unconfirmed (absent).
  • Claimed { claim_txid, claim_depth?, preimage } — recipient claimed; the 32-byte preimage (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_SEQUENCE
  • LedgerDepthKind: 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


Copyright © 2025