Common Patterns
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
}