Api

Common Patterns

Master complex data structures and avoid common mistakes

This guide covers common data structures and patterns you'll encounter when working with the Hydra API.

Fee Structures

FeeOption (for channel operations)

When opening, depositing, withdrawing, or closing channels, you need to specify a FeeOption. This is a protobuf oneof type that can be one of:

interface FeeOption {
  low?: {}           // Use low fee rate
  medium?: {}        // Use medium fee rate
  high?: {}          // Use high fee rate
  custom?: {         // Use custom fee rate
    feeRate: FeeRate
  }
}

Examples:

// Low fee
const feeOption = { low: {} }

// Medium fee (recommended default)
const feeOption = { medium: {} }

// High fee
const feeOption = { high: {} }

// Custom fee (advanced)
const feeOption = {
  custom: {
    feeRate: {
      maxTipFeePerUnit: { value: '1000000000' },  // 1 Gwei
      maxFeePerUnit: { value: '2000000000' }       // 2 Gwei
    }
  }
}

In Practice (grpcWebClient helper):

// The grpcWebClient automatically converts string to FeeOption
await hydraGrpcClient.openChannel(
  network,
  nodeId,
  assetAmounts,
  'medium'  // Automatically converted to { medium: {} }
)

FeeRate (for custom fees and rental transactions)

FeeRate is used for EVM transactions and rental payments:

interface FeeRate {
  maxTipFeePerUnit?: U256String  // Priority fee (tip to miner)
  maxFeePerUnit?: U256String     // Max total fee per gas
}

interface U256String {
  value: string  // String representation of uint256
}

Example:

const feeRate = {
  maxTipFeePerUnit: { value: '1000000000' },    // 1 Gwei tip
  maxFeePerUnit: { value: '10000000000' }       // 10 Gwei max
}

Common Values (in Wei):

// Low priority
const lowFee = {
  maxTipFeePerUnit: { value: '100000000' },     // 0.1 Gwei
  maxFeePerUnit: { value: '1000000000' }        // 1 Gwei
}

// Medium priority (recommended)
const mediumFee = {
  maxTipFeePerUnit: { value: '1000000000' },    // 1 Gwei
  maxFeePerUnit: { value: '10000000000' }       // 10 Gwei
}

// High priority
const highFee = {
  maxTipFeePerUnit: { value: '2000000000' },    // 2 Gwei
  maxFeePerUnit: { value: '50000000000' }       // 50 Gwei
}

ChainFee (from fee estimates)

Fee estimates return ChainFee which includes base fee:

interface ChainFee {
  baseFeePerUnit?: U256String     // Current base fee
  maxTipFeePerUnit?: U256String   // Suggested tip
  maxFeePerUnit?: U256String      // Suggested max fee
}

Example:

const feeEstimates = await hydraGrpcClient.getFeeEstimates(network)

console.log('Fee estimates:', {
  low: feeEstimates.low,
  medium: feeEstimates.medium,
  high: feeEstimates.high
})

Amount Structures

DecimalString

All monetary amounts use DecimalString for precision:

interface DecimalString {
  value: string  // String representation (e.g., '1.5', '0.001')
}

Examples:

// 1.5 ETH
const amount = { value: '1.5' }

// 0.001 BTC
const amount = { value: '0.001' }

// 100 USDC (assuming 6 decimals, this is displayed value)
const amount = { value: '100' }

SendAmount

For operations that send funds (open channel, deposit, send payment):

interface SendAmount {
  exact?: {          // Send exact amount
    amount: DecimalString
  }
  all?: {}          // Send all available balance
}

Examples:

// Send exact amount
const sendAmount = {
  exact: {
    amount: { value: '1.5' }
  }
}

// Send all
const sendAmount = {
  all: {}
}

WithdrawAmount

For withdrawing from channels (different from deposits):

interface WithdrawAmount {
  selfWithdrawal: SendAmount       // Amount to withdraw to self
  counterpartyWithdrawal: SendAmount  // Amount to withdraw to counterparty
}

Example:

const withdrawAmount = {
  selfWithdrawal: {
    exact: { amount: { value: '0.5' } }
  },
  counterpartyWithdrawal: {
    exact: { amount: { value: '0.0' } }
  }
}

DualFundAmount

For dual-funded channel operations:

interface DualFundAmount {
  local: DecimalString      // Amount from local party
  remote: DecimalString     // Amount from remote party
  selfDeposit: DecimalString  // Total self deposit
}

Rental Patterns

RentalOption

When renting a channel, you must specify payment details:

interface RentalOption {
  payment?: {
    rentalTxFeeRate: FeeRate      // Fee for rental transaction
    paymentNetwork: Network        // Network to pay on
    paymentAssetId: string        // Asset to pay with
    paymentMethod: PaymentMethod  // ONCHAIN or OFFCHAIN
  }
  dualFund?: {
    selfAmount: DecimalString     // Amount to dual-fund
  }
}

enum PaymentMethod {
  ONCHAIN = 0,   // Pay with onchain transaction
  OFFCHAIN = 1   // Pay with Lightning/channel payment
}

Example (Offchain Payment):

const rentalOption = {
  payment: {
    rentalTxFeeRate: {
      maxTipFeePerUnit: { value: '1000000000' },
      maxFeePerUnit: { value: '2000000000' }
    },
    paymentNetwork: ethereumNetwork,
    paymentAssetId: '0x0000000000000000000000000000000000000000', // ETH
    paymentMethod: PaymentMethod.OFFCHAIN
  }
}

await hydraGrpcClient.rentChannel(
  network,
  assetId,
  '86400',        // 1 day in seconds
  '1.0',          // Amount of inbound liquidity
  rentalOption
)

Market/Orderbook Patterns

OrderbookCurrency

Identifies an asset in the orderbook:

interface OrderbookCurrency {
  protocol: Protocol     // BITCOIN or EVM
  networkId: string      // Network ID
  assetId: string       // Asset ID
}

Example:

const baseCurrency = {
  protocol: Protocol.EVM,
  networkId: '11155111',  // Ethereum Sepolia
  assetId: '0x0000000000000000000000000000000000000000'  // ETH
}

const quoteCurrency = {
  protocol: Protocol.BITCOIN,
  networkId: '0a03cf40',  // Bitcoin Signet
  assetId: 'native'       // BTC
}

// Initialize market
await hydraGrpcClient.initMarket(baseCurrency, quoteCurrency)

SwapAmount

For swap operations:

interface SwapAmount {
  from?: { amount: DecimalString }  // Specify input amount
  to?: { amount: DecimalString }    // Specify output amount
}

Example:

// Swap exactly 1.0 ETH for BTC
const swapAmount = {
  from: { amount: { value: '1.0' } }
}

// Get exactly 0.1 BTC for ETH
const swapAmount = {
  to: { amount: { value: '0.1' } }
}

Channel Status Patterns

ChannelStatus (enum)

enum ChannelStatus {
  INACTIVE = 0,
  ACTIVE = 1,
  UPDATING = 2,
  CLOSED = 3,
  CLOSED_REDEEMABLE = 4
}

AssetChannelStatus (detailed status)

Each asset in a channel has a detailed status:

interface AssetChannelStatus {
  cooperativelyOpening?: {}
  opening?: {}
  cooperativelyUpdating?: {}
  updating?: {}
  cooperativelyClosing?: {}
  closing?: { closedAtBlock: string }
  forceClosing?: {
    forceClosedAtBlock?: string
    disputer: Disputer
    disputeDeadline?: Deadline
  }
  closedRedeemable?: {}
  closed?: {}
  inactive?: {}
  activeSending?: {}
  activeReceiving?: {}
  active?: {}
  recovering?: {}
}

Common Mistakes to Avoid

❌ Wrong: Passing string directly as fee

await hydraGrpcClient.depositChannel(network, channelId, amounts, 'medium')
// This might work with helper, but raw gRPC call needs object

✅ Correct: Using FeeOption object

const request = {
  network,
  channelId,
  assetAmounts,
  feeOption: { medium: {} }  // Proper FeeOption structure
}

❌ Wrong: FeeRate with single value

const feeRate = { value: '10' }  // Incorrect structure

✅ Correct: FeeRate with proper fields

const feeRate = {
  maxTipFeePerUnit: { value: '1000000000' },
  maxFeePerUnit: { value: '2000000000' }
}

❌ Wrong: Amount as number

const amount = 1.5  // Numbers lose precision

✅ Correct: Amount as DecimalString

const amount = { value: '1.5' }  // Always use string

❌ Wrong: Missing nested structure

const sendAmount = { amount: '1.5' }  // Missing exact/all wrapper

✅ Correct: Proper SendAmount structure

const sendAmount = {
  exact: {
    amount: { value: '1.5' }
  }
}

Helper Functions

The grpcWebClient provides helpful conversions:

// Converts string fee rate to FeeOption
private convertFeeRateToFeeOption(feeRate: string): FeeOption {
  switch (feeRate) {
    case 'low': return { low: {} }
    case 'medium': return { medium: {} }
    case 'high': return { high: {} }
    default: return { medium: {} }
  }
}

// Automatically transforms asset amounts
// From: { assetId: { exact: { amount: '1.0' } } }
// To:   { assetId: { exact: { amount: { value: '1.0' } } } }

Common Error Messages

"fee rate is required"

Cause: Incorrect FeeRate structure in rental operations

Wrong:

rentalOption: {
  payment: {
    rentalTxFeeRate: { value: '10' }  // ❌ Missing maxTipFeePerUnit and maxFeePerUnit
  }
}

Correct:

rentalOption: {
  payment: {
    rentalTxFeeRate: {
      maxTipFeePerUnit: { value: '1000000000' },  // ✅ 1 Gwei
      maxFeePerUnit: { value: '2000000000' }      // ✅ 2 Gwei
    }
  }
}

"requestEncoder(...).finish is not a function"

Cause: Using placeholder encoder instead of proper protobuf encoder

Wrong:

await this.grpcCall(
  'hydra_app.NodeService',
  'OpenChannel',
  request,
  (req) => new Uint8Array()  // ❌ Placeholder encoder
)

Correct:

import { OpenChannelRequest, OpenChannelResponse } from '@/proto/node'

await this.grpcCall<OpenChannelRequest, OpenChannelResponse>(
  'hydra_app.NodeService',
  'OpenChannel',
  request,
  OpenChannelRequest.encode  // ✅ Proper protobuf encoder
)

"insufficient funds"

Cause: Not enough balance in the specified account

Solution:

// Check balance before operation
const balances = await hydraGrpcClient.getBalances(network)
const ethBalance = balances.balances.find(b => b.assetId === '0x0000...')

if (parseFloat(ethBalance.onchain?.usable?.value || '0') < parseFloat(amountNeeded)) {
  console.error('Insufficient onchain balance')
  // Show error to user or request deposit
}

"peer not found" / "peer not connected"

Cause: Trying to open channel with a peer that isn't connected

Solution:

// Connect to peer first
await hydraGrpcClient.connectToPeer(network, peerUrl)

// Wait a moment for connection to establish
await new Promise(resolve => setTimeout(resolve, 1000))

// Then open channel
await hydraGrpcClient.openChannel(network, nodeId, assetAmounts, 'medium')

TypeScript Type Definitions

Complete Type Hierarchy

// From @/proto/models

/** Network identifier */
interface Network {
  protocol: Protocol      // 0 = BITCOIN, 1 = EVM
  id: string             // Network ID (e.g., '11155111')
}

enum Protocol {
  BITCOIN = 0,
  EVM = 1
}

/** Decimal string for precise amounts */
interface DecimalString {
  value: string  // e.g., '1.5', '0.001'
}

/** 256-bit unsigned integer as string */
interface U256String {
  value: string  // e.g., '1000000000' for 1 Gwei
}

/** Fee option for channel operations (oneof type) */
interface FeeOption {
  low?: {}
  medium?: {}
  high?: {}
  custom?: {
    feeRate: FeeRate
  }
}

/** Fee rate for EVM transactions */
interface FeeRate {
  maxTipFeePerUnit?: U256String   // Priority fee (tip)
  maxFeePerUnit?: U256String      // Max total fee
}

/** Fee estimate from blockchain */
interface ChainFee {
  baseFeePerUnit?: U256String     // Current base fee
  maxTipFeePerUnit?: U256String   // Suggested tip
  maxFeePerUnit?: U256String      // Suggested max
}

/** Amount to send (oneof type) */
interface SendAmount {
  exact?: {
    amount: DecimalString
  }
  all?: {}
}

/** Withdrawal amounts */
interface WithdrawAmount {
  selfWithdrawal: SendAmount
  counterpartyWithdrawal: SendAmount
}

/** Asset amounts map */
type AssetAmounts = {
  [assetId: string]: SendAmount
}

/** Withdrawal amounts map */
type WithdrawAssetAmounts = {
  [assetId: string]: WithdrawAmount
}

Debugging Tips

Inspecting Protobuf Messages

// Log the encoded request before sending
const request = OpenChannelRequest.create({
  network,
  peerUrl: nodeId,
  assetAmounts,
  feeOption: { medium: {} }
})

console.log('Request:', JSON.stringify(request, null, 2))

// Verify encoding works
const encoded = OpenChannelRequest.encode(request).finish()
console.log('Encoded length:', encoded.length)

// Decode to verify
const decoded = OpenChannelRequest.decode(encoded)
console.log('Decoded:', JSON.stringify(decoded, null, 2))

Checking Response Structure

try {
  const response = await hydraGrpcClient.getBalances(network)

  console.log('Response structure:', {
    hasBalances: Array.isArray(response.balances),
    balanceCount: response.balances?.length,
    firstBalance: response.balances?.[0],
    fields: Object.keys(response)
  })
} catch (error: any) {
  console.error('Error details:', {
    message: error.message,
    code: error.code,
    details: error.details,
    stack: error.stack
  })
}

Validating Amount Precision

// DON'T use parseFloat for comparisons
const amount1 = '0.1'
const amount2 = '0.2'
const sum = parseFloat(amount1) + parseFloat(amount2)  // ❌ 0.30000000000000004

// DO use string operations or BigInt for precision
import Big from 'big.js'  // or use a decimal library

const amount1 = new Big('0.1')
const amount2 = new Big('0.2')
const sum = amount1.plus(amount2).toString()  // ✅ '0.3'

Real-World Patterns

Opening a Channel with Error Handling

async function openChannelSafely(
  network: Network,
  nodeId: string,
  assetAmounts: AssetAmounts,
  feeRate: 'low' | 'medium' | 'high'
) {
  try {
    // 1. Check balance
    const balances = await hydraGrpcClient.getBalances(network)
    for (const [assetId, amount] of Object.entries(assetAmounts)) {
      const balance = balances.balances.find(b => b.assetId === assetId)
      const available = parseFloat(balance?.onchain?.usable?.value || '0')
      const needed = parseFloat(amount.exact?.amount?.value || '0')

      if (available < needed) {
        throw new Error(`Insufficient ${assetId}: have ${available}, need ${needed}`)
      }
    }

    // 2. Connect to peer if not connected
    try {
      await hydraGrpcClient.connectToPeer(network, nodeId)
      await new Promise(resolve => setTimeout(resolve, 1000))
    } catch (err: any) {
      if (!err.message.includes('already connected')) {
        throw err
      }
    }

    // 3. Open channel
    const response = await hydraGrpcClient.openChannel(
      network,
      nodeId,
      assetAmounts,
      feeRate
    )

    console.log('✅ Channel opened:', response.channelId)
    return response
  } catch (error: any) {
    console.error('❌ Failed to open channel:', error.message)
    throw error
  }
}

Renting a Channel with Fee Calculation

async function rentChannelWithFeeEstimate(
  network: Network,
  assetId: string,
  lifetimeSeconds: string,
  amount: string
) {
  // 1. Get fee estimates
  const feeEstimates = await hydraGrpcClient.getFeeEstimates(network)

  // 2. Use medium fee for rental transaction
  const rentalOption = {
    payment: {
      rentalTxFeeRate: {
        maxTipFeePerUnit: feeEstimates.medium?.maxTipFeePerUnit,
        maxFeePerUnit: feeEstimates.medium?.maxFeePerUnit
      },
      paymentNetwork: network,
      paymentAssetId: assetId,
      paymentMethod: PaymentMethod.OFFCHAIN
    }
  }

  // 3. Get rental fee estimate
  const feeEstimate = await hydraGrpcClient.rentChannelFeeEstimate(
    network,
    assetId,
    lifetimeSeconds,
    amount,
    rentalOption
  )

  console.log(`Rental fee: ${feeEstimate.fee?.value}`)

  // 4. Confirm with user before proceeding
  const confirmed = await confirmWithUser(
    `Rent ${amount} inbound liquidity for ${lifetimeSeconds}s?` +
    `\nFee: ${feeEstimate.fee?.value}`
  )

  if (!confirmed) return

  // 5. Rent the channel
  const response = await hydraGrpcClient.rentChannel(
    network,
    assetId,
    lifetimeSeconds,
    amount,
    rentalOption
  )

  return response
}

See Also


Copyright © 2025