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: {
      priorityFeePerUnit: { 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 Lease transactions)

FeeRate is the per-unit fee rate used by EVM transactions, Bitcoin transactions (max_fee_per_unit maps to sat/vbyte there), and Lease API payment flows:

interface FeeRate {
  maxFeePerUnit: U256String       // Maximum fee per gas/weight unit
  priorityFeePerUnit: U256String  // Priority fee per gas/weight unit
}

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

Both fields are required (the proto defines them as non-optional). Pass { value: "0" } if you genuinely want zero priority.

Example:

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

Common Values (in Wei):

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

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

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

ChainFee (from fee estimates)

blockchain.GetFeeEstimates returns a FeeEstimate with a ChainFee for each priority level:

interface FeeEstimate {
  low: ChainFee
  medium: ChainFee
  high: ChainFee
}

interface ChainFee {
  baseFeePerUnit: U256String       // Current base fee on the network
  feeRate: FeeRate                 // The fee rate (max + priority)
  effectiveFeePerUnit: U256String  // Effective fee per gas/weight unit
}

Example:

const resp = await blockchain.getFeeEstimates(req, {})
const fe = resp.getFeeEstimate()
console.log('Medium fee rate:', fe?.getMedium()?.getFeeRate()?.toObject())
console.log('Effective:', fe?.getMedium()?.getEffectiveFeePerUnit()?.getValue())

When you need a FeeRate for a custom transaction, the easiest path is: call GetFeeEstimates, pick a priority, and use chainFee.getFeeRate() directly.

Amount Structures

DecimalString — the single most-misunderstood type

Every monetary amount in the Hydra API is a DecimalString:

interface DecimalString {
  value: string  // e.g., '1.5', '0.001', '0.00000001'
}

The rule, once and for all: DecimalString.value is always in whole-asset, human-readable units. Never satoshis. Never wei. Never base units. The string "0.001" means 0.001 of the asset, full stop.

This is the opposite of what most chain-native APIs do. If you've used Bitcoin Core or web3.js, your reflex will be to multiply by 10^decimals. Don't. Hydra does that conversion internally.

Worked examples per asset family

Assetvalue: "0.001" meansvalue: "100" meansvalue: "0.00000001" means
BTC (8 decimals)0.001 BTC = 100,000 sats100 BTC0.00000001 BTC = 1 sat
ETH (18 decimals)0.001 ETH = 10¹⁵ wei100 ETH0.00000001 ETH = 10¹⁰ wei
USDC (6 decimals)0.001 USDC100 USDC0.00000001 USDC (sub-cent — usually meaningless)
HDN (18 decimals)0.001 HDN100 HDN0.00000001 HDN

The asset's decimals field (from AssetService.GetAsset) tells you the smallest meaningful fraction, but does not affect what you put in DecimalString.value — it's purely informational, useful for UI rounding.

When you have a base-unit amount and need to convert

If your input came from a chain-native source (a raw transaction, a contract event, a satoshi count from a faucet), you must convert to human units before passing it to Hydra:

import Big from 'big.js'

// 50,000 sats → "0.0005" BTC
const sats = '50000'
const btc = new Big(sats).div(new Big(10).pow(8)).toString()
// → "0.0005"

// 1.5 × 10¹⁸ wei → "1.5" ETH
const wei = '1500000000000000000'
const eth = new Big(wei).div(new Big(10).pow(18)).toString()
// → "1.5"

When you have a DecimalString from Hydra and need base units

The reverse: if you're feeding a Hydra-returned amount into a chain RPC or a contract call, you'll need base units:

import Big from 'big.js'

// "0.0005" BTC → 50,000 sats
const btc = '0.0005'
const sats = new Big(btc).times(new Big(10).pow(8)).toFixed(0)
// → "50000"

Use a decimal library, not floats. parseFloat("0.1") + parseFloat("0.2") is 0.30000000000000004, not 0.3. Across many trades that error compounds. JavaScript: big.js or decimal.js. Go: math/big's *big.Rat or shopspring/decimal. Rust: rust_decimal.

DecimalString vs U256String — when each appears

Both wrap a string. They are not interchangeable.

TypeMeansWhere it shows up
DecimalStringHuman-readable decimal value of an assetAll Amount fields, balances, prices, fees on the Lease API, anything denominated in an asset
U256StringRaw 256-bit unsigned integer in base unitsFeeRate.max_fee_per_unit, FeeRate.priority_fee_per_unit, ChainFee.base_fee_per_unit, ChainFee.effective_fee_per_unit

The rule of thumb: U256String only shows up inside fee-rate / gas-price types, where the unit is sat/vbyte (Bitcoin) or wei per gas (EVM). Everywhere else you see a string-based amount, it's DecimalString and human-readable.

// FeeRate uses U256String — base-unit values
const txFeeRate = {
  maxFeePerUnit:      { value: '50' },   // 50 sat/vbyte (Bitcoin)
                                          //  OR 50 wei/gas (EVM, but you'd usually use much larger)
  priorityFeePerUnit: { value: '5' },
}

// Amount uses DecimalString — human-readable
const amount = {
  exact: { amount: { value: '0.001' } },  // 0.001 BTC = 100,000 sats
}

Common bug: filling DecimalString.value with "100000" thinking it's sats. That's actually 100,000 BTC. On testnet you'll get INVALID_ARGUMENT: insufficient balance. On mainnet that's the kind of bug that empties wallets.

Orderbook prices

Orderbook prices (LimitOrder.price, Trade.price, Trade.final_price, etc.) are also DecimalString, expressed as quote-per-base in human units.

For a BTC/USDC pair (base=BTC, quote=USDC):

priceMeaning
"67000"1 BTC = 67,000 USDC
"67000.5"1 BTC = 67,000.5 USDC
"0.000015"1 BTC = 0.000015 USDC (almost certainly a bug — invert your base/quote)

For an ETH/BTC pair (base=ETH, quote=BTC):

priceMeaning
"0.04"1 ETH = 0.04 BTC
"25"1 ETH = 25 BTC (a bug; you've inverted)

If your bot ever computes "is this trade profitable?" it must know which side is base and which is quote. Don't infer from asset names — pull OrderbookCurrency.base and OrderbookCurrency.quote from the market info and use those.

Negative amounts and zero

UseAllowed?
value: "0"✅ Valid wherever a non-negative amount is expected (e.g. AssetLiquidity.server_amount when only client funds the channel)
value: "-1"❌ Always rejected with INVALID_ARGUMENT
value: "" (empty)❌ Rejected — set the field explicitly to "0" if you mean zero
value: "1e-8" (scientific)❌ Rejected — the parser expects plain decimal notation; use "0.00000001"
Leading zeros ("0.0010")✅ Accepted; canonicalised on the server side

Precision-arithmetic checklist for bots

  1. Never use parseFloat / Number() / f64 on DecimalString.value for arithmetic. Display-only is fine.
  2. Compare amounts as strings or via a decimal library. "0.10" === "0.1" is false in plain JS; both libraries normalise.
  3. Round at output, not throughout the pipeline. Carry full precision; only round when displaying to a human.
  4. Watch for trailing zeros from servers. "0.10000000" and "0.1" are equal in value; if you're using strings as map keys, normalise first.
  5. Sanity-check magnitudes. A bot that's about to send "50000" BTC instead of "0.0005" BTC should hit a guard rail (e.g. "reject single trades > 1 BTC unless explicitly authorised").

Amount

For operations that send funds (client.SendTransaction, node.OpenChannel, node.DepositChannel, node.SendChannelPayment, etc.):

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

The proto type is Amount (defined in balance.proto). Older client code may call this SendAmount — that's a legacy name; the wire shape is identical.

Examples:

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

// Send all available
const amount = {
  all: {}
}

WithdrawAmount

For withdrawing assets from a channel — both sides can withdraw in the same on-chain transaction:

interface WithdrawAmount {
  selfWithdrawal: Amount          // Amount to withdraw to your wallet
  counterpartyWithdrawal: Amount  // Amount to withdraw to the counterparty's wallet
}

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
}

Lease Patterns

The old RentalService is gone — service-backed channel liquidity is now provided by LiquidityService (the Lease API). The shapes are different.

Three operations, one fee-payment model

All three Lease RPCs share a common fee-payment shape: pick exactly one of onchain_fee_payment, offchain_fee_payment, or (for liquidity provisioning only) dual_fund_fee_payment.

// Common fee-payment fields, present on all three Lease requests:
interface LeaseFeePayment {
  paymentNetwork: Network
  paymentAssetId: string

  // exactly one of:
  onchainFeePayment?:
    | { utxo: { refundAddress: string } }
    | { account: { senderAddress: string; refundAddress?: string } }
  offchainFeePayment?: {}
  dualFundFeePayment?: {}    // RequestChannelLiquidity only
}
VariantAvailable onWhen to use
offchainFeePaymentLiquidity / Release / Lease ExtensionYou have offchain balance — fastest, lowest overhead
onchainFeePayment.utxoAll three (Bitcoin-style)Fee paid on-chain, refund-address required
onchainFeePayment.accountAll three (EVM-style)Fee paid on-chain from an EVM account
dualFundFeePaymentLiquidity onlySettle the fee inside the dual-fund flow when client_amount > 0

RequestChannelLiquidity (provision a channel)

interface RequestChannelLiquidityRequest extends LeaseFeePayment {
  network: Network
  operation: ChannelLiquidityRequestOperation  // open / deposit / deposit_any / open_or_deposit
  leaseDurationSeconds?: number  // required when any asset has server_amount > 0
  txFeeRate: FeeRate
}

Example — open a channel with 0.001 BTC client-side, offchain fee:

const req = {
  network: { protocol: 1, id: '0a03cf40' },          // Bitcoin Signet
  operation: {
    open: {
      assetLiquidity: {
        BTC: {
          serverAmount: { value: '0' },              // not asking the service to provide BTC
          clientAmount: { value: '0.001' },          // we're providing it
        },
      },
    },
  },
  // leaseDurationSeconds omitted — server_amount is 0
  txFeeRate: {
    maxFeePerUnit:      { value: '50' },
    priorityFeePerUnit: { value: '5' },
  },
  paymentNetwork: { protocol: 1, id: '0a03cf40' },
  paymentAssetId: 'BTC',
  offchainFeePayment: {},
}

const { channelId, txid } = await liquidity.requestChannelLiquidity(req, {})

RequestChannelRelease (withdraw or close)

interface ChannelReleaseOperation {
  // exactly one:
  withdraw?:          { channelId: string; assetIds: string[] }
  cooperativeClose?:  { channelId: string }
}

interface RequestChannelReleaseRequest extends LeaseFeePayment {
  network: Network
  operation: ChannelReleaseOperation
  txFeeRate: FeeRate
  // dualFundFeePayment is NOT valid here
}

RequestChannelLeaseExtension (extend an asset's lease)

interface RequestChannelLeaseExtensionRequest extends LeaseFeePayment {
  network: Network
  channelId: string
  assetId: string
  leaseExtensionSeconds: number   // delta — added to the current expiry
  // dualFundFeePayment is NOT valid here
}

leaseExtensionSeconds is a delta, not a target absolute expiry. Common bug: passing Date.now() + days * 86400 instead of just days * 86400.

Rules of thumb

  1. Always estimate first. Each operation has a paired Estimate* RPC that takes the same request and returns just the fee.
  2. Set leaseDurationSeconds only if needed. Required when any asset has server_amount > 0; otherwise omit.
  3. Use deposit / open_or_deposit to reuse channels. Saves the cost of opening a new one.
  4. dualFundFeePayment is only valid on RequestChannelLiquidity. Setting it on Release or Lease Extension returns INVALID_ARGUMENT.

See the Lease API reference for the full proto-shape and per-RPC examples in TypeScript / Go / Rust.

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.PROTOCOL_EVM,
  networkId: '11155111',  // Ethereum Sepolia
  assetId: '<asset_id>',  // Token contract for ERC-20s, or asset.GetNativeAsset().id for native ETH
}

const quoteCurrency = {
  protocol: Protocol.PROTOCOL_BITCOIN,
  networkId: '0a03cf40',  // Bitcoin Signet
  assetId: '<asset_id>',  // Use asset.GetNativeAsset({ network: signet }).id for native BTC
}

// Initialize market
await orderbook.initMarket({ base: baseCurrency, quote: 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 = {
  priorityFeePerUnit: { 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 Amount structure

const amount = {
  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: A FeeRate was passed as a single value instead of the two-field shape.

Wrong:

txFeeRate: { value: '10' }   // ❌ FeeRate isn't a DecimalString

Correct:

txFeeRate: {
  maxFeePerUnit:      { value: '50' },   // ✅ U256String
  priorityFeePerUnit: { value: '5' },    // ✅ U256String
}

Both fields are required. Use { value: '0' } if you genuinely want zero priority.

"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

// Generated from the .proto files in /proto

/** Network identifier */
interface Network {
  protocol: Protocol      // PROTOCOL_BITCOIN = 1, PROTOCOL_EVM = 2
  id: string              // Magic bytes (hex) for Bitcoin, decimal chain ID for EVM
}

enum Protocol {
  PROTOCOL_UNSPECIFIED = 0,
  PROTOCOL_BITCOIN = 1,
  PROTOCOL_EVM = 2,
}

/** Decimal string — human-readable, NOT base units */
interface DecimalString {
  value: string  // e.g., '1.5', '0.001' — always in whole-asset units
}

/** 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 (per gas/weight unit) */
interface FeeRate {
  maxFeePerUnit: U256String       // Max fee per gas/weight unit
  priorityFeePerUnit: U256String  // Priority fee per gas/weight unit
}

/** Single ChainFee inside a FeeEstimate */
interface ChainFee {
  baseFeePerUnit: U256String       // Current network base fee
  feeRate: FeeRate                 // Suggested rate
  effectiveFeePerUnit: U256String  // Effective fee per unit
}

/** Result of blockchain.GetFeeEstimates */
interface FeeEstimate {
  low: ChainFee
  medium: ChainFee
  high: ChainFee
}

/** Amount to send (oneof type) — used by SendTransaction, OpenChannel, etc. */
interface Amount {
  exact?: {
    amount: DecimalString
  }
  all?: {}
}

/** Withdrawal amounts (both sides withdraw in the same tx) */
interface WithdrawAmount {
  selfWithdrawal: Amount
  counterpartyWithdrawal: Amount
}

/** Asset amounts map — used by OpenChannel, DepositChannel, etc. */
type AssetAmounts = {
  [assetId: string]: Amount
}

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

/** Used by orderbook and swap RPCs (separate from Network) */
interface OrderbookCurrency {
  protocol: Protocol
  networkId: string  // Same shape as Network.id
  assetId: string
}

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

Provisioning channel liquidity (Lease) with fee estimate first

async function provisionLiquidity(
  liquidity: LiquidityServiceClient,
  blockchain: BlockchainServiceClient,
  network: { protocol: number; id: string },
  assetId: string,
  serverAmount: string,
  leaseDays: number,
) {
  // 1. Get current fee estimates from the chain
  const feReq = new GetFeeEstimatesRequest()
  feReq.setNetwork(network)
  const feResp = await blockchain.getFeeEstimates(feReq, {})
  const mediumRate = feResp.getFeeEstimate()?.getMedium()?.getFeeRate()
  if (!mediumRate) throw new Error('no fee estimate available')

  // 2. Build the request — same shape for estimate and execute
  const liqReq = new RequestChannelLiquidityRequest()
  liqReq.setNetwork(network)

  const open = new ChannelLiquidityRequestOperation.Open()
  const al = new AssetLiquidity()
  al.setServerAmount({ value: serverAmount })
  al.setClientAmount({ value: '0' })
  open.getAssetLiquidityMap().set(assetId, al)

  const op = new ChannelLiquidityRequestOperation()
  op.setOpen(open)
  liqReq.setOperation(op)
  liqReq.setLeaseDurationSeconds(leaseDays * 24 * 60 * 60)
  liqReq.setTxFeeRate(mediumRate)
  liqReq.setPaymentNetwork(network)
  liqReq.setPaymentAssetId(assetId)
  liqReq.setOffchainFeePayment(new OffchainFeePayment())

  // 3. Estimate first
  const est = await liquidity.estimateRequestChannelLiquidityFee(liqReq, {})
  console.log(`Service fee: ${est.getFee()?.getValue()} ${assetId}`)

  // 4. (Optional) confirm with operator / threshold check
  if (parseFloat(est.getFee()?.getValue() ?? '0') > 0.001) {
    throw new Error('fee exceeds threshold')
  }

  // 5. Execute
  const result = await liquidity.requestChannelLiquidity(liqReq, {})
  return {
    channelId: result.getChannelId(),
    txid: result.getTxid(),
  }
}

See Also


Copyright © 2025