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) — settle a revealed preimage. Two RPCs, one per facet: SettlePreimage for channel legs (this replaced NodeService.RegisterPreimage) and SettleHtlcPreimage for on-chain HTLC legs.
  • 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.

Settling a revealed preimage

A revealed preimage S can unlock value on two independent facets of a network's node — held channel hashlock payments, and on-chain HTLCs paying this node under sha256(S). Since 2026-08-01 each facet has its own RPC, because they settle independently: they fail independently, and only the on-chain one broadcasts transactions and therefore costs a fee.

On a network with both facets, drive both RPCs. If you relied on SettlePreimage also sweeping on-chain HTLCs (its 2026-06-11 behavior), you must now also call SettleHtlcPreimage.

Settle Preimage

Namespace preimage. Added 2026-06-11 (replaced node.RegisterPreimage); narrowed to channel legs 2026-08-01.

Registers a revealed preimage and settles every channel leg it unlocks on the given network: persists it durably, claims held channel hashlock payments, and arms the on-chain settlement path. Idempotent. Costs no fee of its own — any resulting on-chain settlement is the channel's own path.

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. See Node API → Register Preimage (moved).

Settle HTLC Preimage

Added 2026-08-01.

Claims every on-chain HTLC the preimage unlocks on the given network: each one still Locked, paying this node, under sha256(preimage). Idempotent — an HTLC already claimed is simply no longer Locked.

Method: SettleHtlcPreimage

NameTypeRequiredDescription
networkNetworkYESTarget network
payment_preimagestringYESHex-encoded preimage whose hash the claimed HTLCs commit to
fee_optionFeeOptionNOFee for the claim transactions. Absent = the network's default (medium) HTLC fee rate — the same default every other unpinned HTLC operation takes.

Response:

FieldTypeDescription
txidsstring[]One transaction id per HTLC claimed, in no particular order. Empty when the secret unlocks none here — that is a success, not an error.

Each claim is its own transaction. One that cannot be built — already swept, expired and refunded, unfunded for its fee — is skipped rather than withholding the others, so a partial result is normal. Rejected with FAILED_PRECONDITION on a network with no on-chain HTLC facet.

import { PreimageServiceClient } from './proto/PreimageServiceClientPb'
import { SettlePreimageRequest, SettleHtlcPreimageRequest } from './proto/preimage_pb'

const preimage = new PreimageServiceClient('http://localhost:5001')
const network = { protocol: 1, id: '0a03cf40' }
const secret = '9f8e7d6c...'

// 1. Channel legs — no fee of its own.
const chan = new SettlePreimageRequest()
chan.setNetwork(network)
chan.setPaymentPreimage(secret)
await preimage.settlePreimage(chan, {})

// 2. On-chain HTLC legs — broadcasts one claim tx per HTLC.
const onchain = new SettleHtlcPreimageRequest()
onchain.setNetwork(network)
onchain.setPaymentPreimage(secret)
const res = await preimage.settleHtlcPreimage(onchain, {})
console.log('claimed:', res.getTxidsList())   // [] = nothing to claim here

HtlcService RPCs

Namespace htlc. Works across Bitcoin (P2WSH / Taproot) and EVM / Tron (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