Api

API Examples

Client implementation examples in TypeScript and Rust

Real-world client implementation examples showing how to interact with the Hydra API from TypeScript (gRPC-Web) and Rust (gRPC).

Table of Contents


Basic gRPC Call

TypeScript (gRPC-Web)

Making a gRPC-Web request requires manual framing:

import { BinaryWriter, BinaryReader } from "@bufbuild/protobuf/wire"
import { GetNetworksRequest, GetNetworksResponse } from '@/proto/app'

async function getNetworks(): Promise<Network[]> {
  const baseUrl = 'http://localhost:5001'
  const url = `${baseUrl}/hydra_app.AppService/GetNetworks`

  // 1. Create and encode the request
  const request: GetNetworksRequest = {}
  const messageBytes = GetNetworksRequest.encode(request).finish()

  // 2. Add gRPC-Web framing (5 bytes + message)
  const frame = new Uint8Array(5 + messageBytes.length)
  const view = new DataView(frame.buffer)

  view.setUint8(0, 0)                          // Compression flag: 0 = no compression
  view.setUint32(1, messageBytes.length, false) // Message length (big-endian)
  frame.set(messageBytes, 5)                    // Message data

  // 3. Make the HTTP request
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/grpc-web+proto',
      'Accept': 'application/grpc-web+proto',
      'X-Grpc-Web': '1',
    },
    body: frame
  })

  // 4. Decode the response
  const responseData = await response.arrayBuffer()
  const responseBytes = new Uint8Array(responseData, 5) // Skip 5-byte frame
  const reader = new BinaryReader(responseBytes)
  const decoded = GetNetworksResponse.decode(reader)

  return decoded.networks || []
}

Using the client:

import { hydraGrpcClient } from '@/services/grpcWebClient'

// Get networks
const networks = await hydraGrpcClient.getNetworks()
console.log('Available networks:', networks)

// Get balances for a network
const balances = await hydraGrpcClient.getBalances()
console.log('Balances:', balances)

// Get channels
const channels = await hydraGrpcClient.getChannels()
console.log('Channels:', channels)
Rust (gRPC with Tonic)

Rust uses the tonic crate which handles framing automatically:

use tonic::Request;

// Generated from proto files
pub mod hydra_app {
    tonic::include_proto!("hydra_app");
}

use hydra_app::{
    app_service_client::AppServiceClient,
    GetNetworksRequest,
};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Connect to the Hydra gRPC server
    let mut client = AppServiceClient::connect("http://localhost:5001").await?;

    // Make the request
    let request = Request::new(GetNetworksRequest {});
    let response = client.get_networks(request).await?;

    let networks = response.into_inner().networks;
    println!("Available networks: {:?}", networks);

    Ok(())
}

Using the client for multiple operations:

use hydra_app::{
    app_service_client::AppServiceClient,
    wallet_service_client::WalletServiceClient,
    node_service_client::NodeServiceClient,
    GetNetworksRequest,
    GetBalancesRequest,
    GetChannelsRequest,
};

async fn get_hydra_data() -> Result<(), Box<dyn std::error::Error>> {
    // Create clients
    let mut app_client = AppServiceClient::connect("http://localhost:5001").await?;
    let mut wallet_client = WalletServiceClient::connect("http://localhost:5001").await?;
    let mut node_client = NodeServiceClient::connect("http://localhost:5001").await?;

    // Get networks
    let networks = app_client
        .get_networks(Request::new(GetNetworksRequest {}))
        .await?
        .into_inner()
        .networks;

    println!("Networks: {:?}", networks);

    // Get balances for first network
    if let Some(network) = networks.first() {
        let balances = wallet_client
            .get_balances(Request::new(GetBalancesRequest {
                network: Some(network.clone()),
            }))
            .await?
            .into_inner()
            .balances;

        println!("Balances: {:?}", balances);

        // Get channels
        let channels = node_client
            .get_channels(Request::new(GetChannelsRequest {
                network: Some(network.clone()),
            }))
            .await?
            .into_inner()
            .channels;

        println!("Channels: {:?}", channels);
    }

    Ok(())
}

Protobuf Encoding/Decoding

TypeScript

Encoding Request Messages:

import { GetBalancesRequest } from '@/proto/wallet'
import { Network, Protocol } from '@/proto/models'

// Create a request object
const network: Network = {
  protocol: Protocol.EVM,
  id: '11155111'  // Ethereum Sepolia
}

const request: GetBalancesRequest = { network }

// Encode to binary
const encoder = GetBalancesRequest.encode
const messageBytes = encoder(request).finish()

console.log('Encoded message length:', messageBytes.length)
console.log('Message bytes:', messageBytes)

Decoding Response Messages:

import { GetBalancesResponse } from '@/proto/wallet'
import { BinaryReader } from "@bufbuild/protobuf/wire"

// Assume we have response data from the server
const responseData: Uint8Array = ... // From fetch response

// Skip gRPC-Web frame header (5 bytes)
const responseBytes = new Uint8Array(responseData.buffer, 5)

// Decode using protobuf
const reader = new BinaryReader(responseBytes)
const decoded = GetBalancesResponse.decode(reader)

console.log('Decoded balances:', decoded.balances)

// Access balance data
for (const [assetId, balance] of Object.entries(decoded.balances)) {
  console.log(`Asset: ${assetId}`)
  console.log(`  Onchain: ${balance.onChain?.usable?.value}`)
  console.log(`  Offchain: ${balance.offChain?.freeLocal?.value}`)
}
Rust

Creating Request Messages:

use hydra_app::{Network, Protocol, GetBalancesRequest};

// Create a network object
let network = Network {
    protocol: Protocol::Evm as i32,
    id: "11155111".to_string(),  // Ethereum Sepolia
};

// Create request
let request = GetBalancesRequest {
    network: Some(network),
};

println!("Request: {:?}", request);

Accessing Response Data:

use hydra_app::{GetBalancesResponse, wallet_service_client::WalletServiceClient};
use tonic::Request;

async fn get_and_display_balances(
    client: &mut WalletServiceClient<tonic::transport::Channel>,
    network: Network,
) -> Result<(), Box<dyn std::error::Error>> {
    let request = Request::new(GetBalancesRequest {
        network: Some(network),
    });

    let response = client.get_balances(request).await?;
    let balances = response.into_inner().balances;

    // Access balance data
    for (asset_id, balance) in balances.iter() {
        println!("Asset: {}", asset_id);

        if let Some(onchain) = &balance.on_chain {
            if let Some(usable) = &onchain.usable {
                println!("  Onchain: {}", usable.value);
            }
        }

        if let Some(offchain) = &balance.off_chain {
            if let Some(free_local) = &offchain.free_local {
                println!("  Offchain: {}", free_local.value);
            }
        }
    }

    Ok(())
}

Request Framing (TypeScript)

Note: Frame handling is automatic in Rust with the tonic crate. This section only applies to TypeScript gRPC-Web clients.

gRPC-Web Frame Structure

/**
 * gRPC-Web frame format:
 *
 * Byte 0:     Compression flag (0 = no compression, 1 = compressed)
 * Bytes 1-4:  Message length (32-bit big-endian integer)
 * Bytes 5+:   Protobuf message data
 */

function createGrpcWebFrame(messageBytes: Uint8Array): Uint8Array {
  const frame = new Uint8Array(5 + messageBytes.length)
  const view = new DataView(frame.buffer)

  // Byte 0: Compression flag
  view.setUint8(0, 0)  // No compression

  // Bytes 1-4: Message length (big-endian)
  view.setUint32(1, messageBytes.length, false)

  // Bytes 5+: Message data
  frame.set(messageBytes, 5)

  return frame
}

// Example usage
const request = OpenChannelRequest.encode({ network, nodeId, assetAmounts, feeOption }).finish()
const framedRequest = createGrpcWebFrame(request)

console.log('Frame size:', framedRequest.length)
console.log('Message size:', request.length)
console.log('Frame overhead:', 5, 'bytes')

Response Frame Parsing

function parseGrpcWebFrame(data: ArrayBuffer): Uint8Array {
  const view = new DataView(data)

  // Byte 0: Compression flag
  const compressed = view.getUint8(0)
  if (compressed !== 0) {
    throw new Error('Compressed responses not supported')
  }

  // Bytes 1-4: Message length
  const messageLength = view.getUint32(1, false)  // big-endian

  console.log('Frame info:')
  console.log('  Compressed:', compressed)
  console.log('  Message length:', messageLength)

  // Bytes 5+: Message data
  return new Uint8Array(data, 5, messageLength)
}

Response Handling

Checking gRPC Status

async function makeGrpcCall(url: string, frame: Uint8Array) {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/grpc-web+proto',
      'Accept': 'application/grpc-web+proto',
      'X-Grpc-Web': '1',
    },
    body: frame
  })

  // Check gRPC status headers
  const grpcStatus = response.headers.get('grpc-status')
  const grpcMessage = response.headers.get('grpc-message')

  console.log('gRPC Status:', grpcStatus)
  console.log('gRPC Message:', grpcMessage)

  if (grpcStatus === '0') {
    // Success
    console.log('✅ gRPC call succeeded')
    return response.arrayBuffer()
  } else {
    // Error
    throw new Error(`gRPC error: status=${grpcStatus}, message=${grpcMessage}`)
  }
}

Handling Different Response Types

async function handleResponse<TResponse>(
  response: Response,
  decoder: (reader: BinaryReader) => TResponse
): Promise<TResponse> {
  const grpcStatus = response.headers.get('grpc-status')
  const contentType = response.headers.get('content-type')

  // Check if we got a proper gRPC response
  if (contentType !== 'application/grpc-web+proto') {
    throw new Error(`Unexpected content type: ${contentType}`)
  }

  if (grpcStatus === '0' || grpcStatus === null) {
    // Parse response body
    const responseData = await response.arrayBuffer()

    if (responseData.byteLength < 5) {
      throw new Error('Response too small for gRPC-Web frame')
    }

    // Skip 5-byte frame header
    const responseBytes = new Uint8Array(responseData, 5)
    const reader = new BinaryReader(responseBytes)

    return decoder(reader)
  }

  throw new Error(`gRPC error: ${grpcStatus}`)
}

// Usage
const response = await fetch(url, { method: 'POST', body: frame, headers: {...} })
const result = await handleResponse(response, GetBalancesResponse.decode)

Event Streaming

TypeScript
import { SubscribeNodeEventsRequest } from '@/proto/event'
import { NodeEvent } from '@/proto/models'

async function subscribeToNodeEvents(
  network: Network,
  onEvent: (event: NodeEvent) => void,
  onError?: (error: Error) => void
): Promise<() => void> {
  const baseUrl = 'http://localhost:5001'
  const url = `${baseUrl}/hydra_app.EventService/SubscribeNodeEvents`

  // Create request
  const request: SubscribeNodeEventsRequest = { network }
  const requestBytes = SubscribeNodeEventsRequest.encode(request).finish()

  // Make streaming request
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/grpc-web+proto',
      'Accept': 'application/grpc-web+proto',
    },
    body: requestBytes
  })

  if (!response.ok) {
    throw new Error(`HTTP ${response.status}: ${response.statusText}`)
  }

  // Get response stream reader
  const reader = response.body?.getReader()
  if (!reader) {
    throw new Error('No response body reader available')
  }

  let isActive = true

  // Unsubscribe function
  const unsubscribe = () => {
    isActive = false
    reader.cancel()
  }

  // Process the stream
  const processStream = async () => {
    try {
      while (isActive) {
        const { done, value } = await reader.read()
        if (done) break

        // Parse gRPC-Web frame and decode NodeEvent
        const eventData = new Uint8Array(value, 5) // Skip 5-byte frame
        const nodeEvent = NodeEvent.decode(new BinaryReader(eventData))

        console.log('📡 Received node event:', nodeEvent)
        onEvent(nodeEvent)
      }
    } catch (error) {
      if (isActive && onError) {
        onError(error as Error)
      }
    }
  }

  // Start processing
  processStream()

  return unsubscribe
}

// Usage
const unsubscribe = await subscribeToNodeEvents(
  network,
  (event) => {
    console.log('Node event:', event)

    if (event.channelOpened) {
      console.log('Channel opened:', event.channelOpened.channelId)
    } else if (event.channelClosed) {
      console.log('Channel closed:', event.channelClosed.channelId)
    } else if (event.paymentReceived) {
      console.log('Payment received:', event.paymentReceived.amount)
    }
  },
  (error) => {
    console.error('Stream error:', error)
  }
)

// Unsubscribe when done
setTimeout(() => unsubscribe(), 60000) // Unsubscribe after 1 minute
Rust
use hydra_app::{
    event_service_client::EventServiceClient,
    SubscribeNodeEventsRequest,
    Network,
};
use tonic::Request;
use tokio_stream::StreamExt;

async fn subscribe_to_node_events(
    network: Network,
) -> Result<(), Box<dyn std::error::Error>> {
    let mut client = EventServiceClient::connect("http://localhost:5001").await?;

    // Create subscription request
    let request = Request::new(SubscribeNodeEventsRequest {
        network: Some(network),
    });

    // Get the streaming response
    let mut stream = client.subscribe_node_events(request).await?.into_inner();

    println!("📡 Subscribed to node events");

    // Process events as they arrive
    while let Some(event) = stream.next().await {
        match event {
            Ok(node_event) => {
                println!("Node event: {:?}", node_event);

                // Handle specific event types
                if let Some(channel_opened) = node_event.channel_opened {
                    println!("Channel opened: {}", channel_opened.channel_id);
                } else if let Some(channel_closed) = node_event.channel_closed {
                    println!("Channel closed: {}", channel_closed.channel_id);
                } else if let Some(payment_received) = node_event.payment_received {
                    println!("Payment received: {:?}", payment_received.amount);
                }
            }
            Err(e) => {
                eprintln!("Stream error: {}", e);
                break;
            }
        }
    }

    println!("Stream ended");
    Ok(())
}

// Usage with tokio::select! for cancellation
use tokio::time::{sleep, Duration};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let network = Network {
        protocol: 1,  // EVM
        id: "11155111".to_string(),  // Ethereum Sepolia
    };

    tokio::select! {
        result = subscribe_to_node_events(network) => {
            result?;
        }
        _ = sleep(Duration::from_secs(60)) => {
            println!("Unsubscribing after 1 minute");
        }
    }

    Ok(())
}

Error Handling

TypeScript
async function safeGrpcCall<TRequest, TResponse>(
  servicePath: string,
  method: string,
  request: TRequest,
  requestEncoder: (message: TRequest, writer?: BinaryWriter) => BinaryWriter,
  responseDecoder: (reader: BinaryReader) => TResponse
): Promise<TResponse> {
  const baseUrl = 'http://localhost:5001'
  const url = `${baseUrl}/${servicePath}/${method}`

  console.log(`🔗 gRPC call: ${servicePath}/${method}`, request)

  try {
    // Encode request
    const messageBytes = requestEncoder(request).finish()

    // Frame request
    const frame = new Uint8Array(5 + messageBytes.length)
    const view = new DataView(frame.buffer)
    view.setUint8(0, 0)
    view.setUint32(1, messageBytes.length, false)
    frame.set(messageBytes, 5)

    // Make request
    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/grpc-web+proto',
        'Accept': 'application/grpc-web+proto',
        'X-Grpc-Web': '1',
      },
      body: frame
    })

    const grpcStatus = response.headers.get('grpc-status')
    const grpcMessage = response.headers.get('grpc-message')

    if (grpcStatus === '0' || grpcStatus === null) {
      // Success - decode response
      const responseData = await response.arrayBuffer()
      const responseBytes = new Uint8Array(responseData, 5)
      const reader = new BinaryReader(responseBytes)

      return responseDecoder(reader)
    } else {
      throw new Error(`gRPC error: status=${grpcStatus}, message=${grpcMessage}`)
    }

  } catch (error: any) {
    // Enhanced error diagnostics
    const errorDetails = {
      url,
      method: `${servicePath}/${method}`,
      originalError: error.message,
      errorType: error.constructor.name,
      timestamp: new Date().toISOString()
    }

    // Network-specific error analysis
    if (error instanceof TypeError && error.message === 'Failed to fetch') {
      errorDetails.diagnosis = [
        `🔌 Network connection failed to ${baseUrl}`,
        `• Check if Hydra backend is running on port 5001`,
        `• Verify CORS is enabled for gRPC-Web requests`,
        `• Test basic connectivity: curl ${baseUrl}`,
      ].join('\n')
    } else if (error.message.includes('CORS')) {
      errorDetails.diagnosis = [
        `🌐 CORS (Cross-Origin Resource Sharing) error`,
        `• Hydra backend needs to allow origin`,
        `• Required CORS headers: Access-Control-Allow-Origin`,
      ].join('\n')
    } else if (error.message.includes('timeout')) {
      errorDetails.diagnosis = [
        `⏱️ Request timeout`,
        `• Hydra backend may be overloaded or unresponsive`,
      ].join('\n')
    }

    console.error(`❌ gRPC call failed:`, errorDetails)
    throw error
  }
}

Retry Logic

async function grpcCallWithRetry<TResponse>(
  callFn: () => Promise<TResponse>,
  maxRetries = 3,
  delayMs = 1000
): Promise<TResponse> {
  let lastError: Error | undefined

  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      console.log(`Attempt ${attempt}/${maxRetries}`)
      return await callFn()
    } catch (error: any) {
      lastError = error
      console.error(`Attempt ${attempt} failed:`, error.message)

      // Don't retry on certain errors
      if (error.message.includes('INVALID_ARGUMENT')) {
        throw error // Bad request, don't retry
      }

      if (attempt < maxRetries) {
        const delay = delayMs * Math.pow(2, attempt - 1) // Exponential backoff
        console.log(`Retrying in ${delay}ms...`)
        await new Promise(resolve => setTimeout(resolve, delay))
      }
    }
  }

  throw lastError || new Error('All retry attempts failed')
}

// Usage
const balances = await grpcCallWithRetry(
  () => hydraGrpcClient.getBalances(),
  3,  // max 3 retries
  1000 // 1 second initial delay
)
Rust

Pattern Matching on gRPC Status:

use tonic::{Code, Status};
use hydra_app::wallet_service_client::WalletServiceClient;

async fn handle_grpc_errors() -> Result<(), Box<dyn std::error::Error>> {
    let mut client = WalletServiceClient::connect("http://localhost:5001").await?;

    match client.get_balances(request).await {
        Ok(response) => {
            println!("✅ Success: {:?}", response.into_inner());
            Ok(())
        }
        Err(status) => {
            match status.code() {
                Code::InvalidArgument => {
                    eprintln!("❌ Invalid argument: {}", status.message());
                    Err("Bad request".into())
                }
                Code::NotFound => {
                    eprintln!("❌ Not found: {}", status.message());
                    Err("Resource not found".into())
                }
                Code::PermissionDenied => {
                    eprintln!("❌ Permission denied: {}", status.message());
                    Err("Access denied".into())
                }
                Code::Unavailable => {
                    eprintln!("⚠️ Service unavailable: {}", status.message());
                    eprintln!("  Check if Hydra backend is running on port 5001");
                    Err("Service unavailable".into())
                }
                Code::DeadlineExceeded => {
                    eprintln!("⏱️ Request timeout: {}", status.message());
                    Err("Timeout".into())
                }
                _ => {
                    eprintln!("❌ gRPC error: {:?} - {}", status.code(), status.message());
                    Err(format!("gRPC error: {}", status.message()).into())
                }
            }
        }
    }
}

Retry Logic with Exponential Backoff:

use tokio::time::{sleep, Duration};

async fn grpc_call_with_retry<F, T, Fut>(
    mut call_fn: F,
    max_retries: u32,
    initial_delay_ms: u64,
) -> Result<T, Box<dyn std::error::Error>>
where
    F: FnMut() -> Fut,
    Fut: std::future::Future<Output = Result<T, Status>>,
{
    let mut last_error: Option<Status> = None;

    for attempt in 1..=max_retries {
        println!("Attempt {}/{}", attempt, max_retries);

        match call_fn().await {
            Ok(result) => {
                println!("✅ Success on attempt {}", attempt);
                return Ok(result);
            }
            Err(status) => {
                eprintln!("❌ Attempt {} failed: {}", attempt, status.message());

                // Don't retry on certain errors
                match status.code() {
                    Code::InvalidArgument | Code::PermissionDenied | Code::NotFound => {
                        eprintln!("Non-retryable error, giving up");
                        return Err(status.into());
                    }
                    _ => {}
                }

                last_error = Some(status);

                if attempt < max_retries {
                    // Exponential backoff
                    let delay = initial_delay_ms * 2u64.pow(attempt - 1);
                    println!("Retrying in {}ms...", delay);
                    sleep(Duration::from_millis(delay)).await;
                }
            }
        }
    }

    Err(last_error.unwrap_or_else(|| Status::unknown("All retries failed")).into())
}

// Usage
use hydra_app::{GetBalancesRequest, GetBalancesResponse};

async fn get_balances_with_retry(
    client: &mut WalletServiceClient<tonic::transport::Channel>,
    network: Network,
) -> Result<GetBalancesResponse, Box<dyn std::error::Error>> {
    grpc_call_with_retry(
        || async {
            client
                .get_balances(Request::new(GetBalancesRequest {
                    network: Some(network.clone()),
                }))
                .await
                .map(|r| r.into_inner())
        },
        3,    // max 3 retries
        1000, // 1 second initial delay
    )
    .await
}

Complete Client Implementation

TypeScript - Generic gRPC-Web Client Class
import { BinaryWriter, BinaryReader } from "@bufbuild/protobuf/wire"

class HydraGrpcWebClient {
  private baseUrl: string

  constructor() {
    this.baseUrl = import.meta.env.VITE_GRPC_URL || 'http://localhost:5001'
  }

  /**
   * Make a gRPC-Web call with proper protobuf encoding
   */
  private async grpcCall<TRequest, TResponse>(
    servicePath: string,
    method: string,
    request: TRequest,
    requestEncoder: (message: TRequest, writer?: BinaryWriter) => BinaryWriter,
    responseDecoder: (reader: BinaryReader) => TResponse
  ): Promise<TResponse> {
    const url = `${this.baseUrl}/${servicePath}/${method}`

    console.log(`🔗 gRPC call: ${servicePath}/${method}`, request)

    // Encode the request using protobuf
    const messageBytes = requestEncoder(request).finish()

    // Add gRPC-Web framing
    const frame = new Uint8Array(5 + messageBytes.length)
    const view = new DataView(frame.buffer)

    view.setUint8(0, 0)                          // No compression
    view.setUint32(1, messageBytes.length, false) // Message length
    frame.set(messageBytes, 5)                    // Message data

    const response = await fetch(url, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/grpc-web+proto',
        'Accept': 'application/grpc-web+proto',
        'X-Grpc-Web': '1',
      },
      body: frame,
    })

    const grpcStatus = response.headers.get('grpc-status')
    const grpcMessage = response.headers.get('grpc-message')

    if (grpcStatus === '0' || grpcStatus === null) {
      const responseData = await response.arrayBuffer()
      const responseBytes = new Uint8Array(responseData, 5)
      const reader = new BinaryReader(responseBytes)

      return responseDecoder(reader)
    } else {
      throw new Error(`gRPC error: status=${grpcStatus}, message=${grpcMessage}`)
    }
  }

  /**
   * Get available networks
   */
  async getNetworks(): Promise<Network[]> {
    const result = await this.grpcCall(
      'hydra_app.AppService',
      'GetNetworks',
      {},
      GetNetworksRequest.encode,
      GetNetworksResponse.decode
    )

    return result.networks || []
  }

  /**
   * Get wallet balances
   */
  async getBalances(): Promise<{ balances: Record<string, any> }> {
    const networks = await this.getNetworks()
    const allBalances: Record<string, any> = {}

    for (const network of networks) {
      try {
        const result = await this.grpcCall(
          'hydra_app.WalletService',
          'GetBalances',
          { network },
          GetBalancesRequest.encode,
          GetBalancesResponse.decode
        )

        if (result.balances) {
          Object.assign(allBalances, result.balances)
        }
      } catch (error: any) {
        console.error(`Failed to get balances for network ${network.id}:`, error.message)
      }
    }

    return { balances: allBalances }
  }

  /**
   * Open a channel
   */
  async openChannel(
    network: Network,
    nodeId: string,
    assetAmounts: Record<string, SendAmount>,
    feeRate: string
  ): Promise<OpenChannelResponse> {
    const feeOption = this.convertFeeRateToFeeOption(feeRate)

    const result = await this.grpcCall(
      'hydra_app.NodeService',
      'OpenChannel',
      { network, nodeId, assetAmounts, feeOption },
      OpenChannelRequest.encode,
      OpenChannelResponse.decode
    )

    console.log('✅ Channel opened successfully:', result)
    return result
  }

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

export const hydraGrpcClient = new HydraGrpcWebClient()
export default hydraGrpcClient

Usage in Application

// Initialize client
import { hydraGrpcClient } from '@/services/grpcWebClient'
import { Protocol } from '@/proto/models'

async function main() {
  try {
    // Get networks
    const networks = await hydraGrpcClient.getNetworks()
    console.log('Networks:', networks)

    const ethNetwork = networks.find(n =>
      n.protocol === Protocol.EVM && n.id === '11155111'
    )

    if (!ethNetwork) {
      console.error('Ethereum Sepolia network not found')
      return
    }

    // Get balances
    const balances = await hydraGrpcClient.getBalances()
    console.log('Balances:', balances)

    // Open a channel
    const assetAmounts = {
      '0x0000000000000000000000000000000000000000': {
        exact: {
          amount: { value: '0.1' }
        }
      }
    }

    await hydraGrpcClient.openChannel(
      ethNetwork,
      '0xnode_id_here',
      assetAmounts,
      'medium'
    )

    console.log('✅ Channel opened')

  } catch (error: any) {
    console.error('Error:', error.message)
  }
}

main()
Rust - Complete Client with Connection Pool

Complete Hydra Client Implementation:

use hydra_app::{
    app_service_client::AppServiceClient,
    wallet_service_client::WalletServiceClient,
    node_service_client::NodeServiceClient,
    Network, Protocol,
    GetNetworksRequest,
    GetBalancesRequest,
    OpenChannelRequest,
    FeeOption,
    SendAmount,
    DecimalString,
};
use tonic::transport::Channel;
use std::collections::HashMap;

/// Hydra gRPC Client
pub struct HydraClient {
    app_client: AppServiceClient<Channel>,
    wallet_client: WalletServiceClient<Channel>,
    node_client: NodeServiceClient<Channel>,
}

impl HydraClient {
    /// Connect to Hydra backend
    pub async fn connect(endpoint: &str) -> Result<Self, Box<dyn std::error::Error>> {
        let channel = Channel::from_shared(endpoint.to_string())?
            .connect()
            .await?;

        Ok(Self {
            app_client: AppServiceClient::new(channel.clone()),
            wallet_client: WalletServiceClient::new(channel.clone()),
            node_client: NodeServiceClient::new(channel),
        })
    }

    /// Get available networks
    pub async fn get_networks(&mut self) -> Result<Vec<Network>, Box<dyn std::error::Error>> {
        let response = self
            .app_client
            .get_networks(GetNetworksRequest {})
            .await?
            .into_inner();

        Ok(response.networks)
    }

    /// Get balances for all networks
    pub async fn get_all_balances(
        &mut self,
    ) -> Result<HashMap<String, HashMap<String, hydra_app::Balance>>, Box<dyn std::error::Error>> {
        let networks = self.get_networks().await?;
        let mut all_balances = HashMap::new();

        for network in networks {
            match self.get_balances(&network).await {
                Ok(balances) => {
                    let network_key = format!("{}:{}", network.protocol, network.id);
                    all_balances.insert(network_key, balances);
                }
                Err(e) => {
                    eprintln!("Failed to get balances for network {}: {}", network.id, e);
                }
            }
        }

        Ok(all_balances)
    }

    /// Get balances for specific network
    pub async fn get_balances(
        &mut self,
        network: &Network,
    ) -> Result<HashMap<String, hydra_app::Balance>, Box<dyn std::error::Error>> {
        let response = self
            .wallet_client
            .get_balances(GetBalancesRequest {
                network: Some(network.clone()),
            })
            .await?
            .into_inner();

        Ok(response.balances)
    }

    /// Open a channel
    pub async fn open_channel(
        &mut self,
        network: &Network,
        node_id: String,
        asset_amounts: HashMap<String, SendAmount>,
        fee_rate: FeeRate,
    ) -> Result<hydra_app::OpenChannelResponse, Box<dyn std::error::Error>> {
        let fee_option = fee_rate.to_fee_option();

        let response = self
            .node_client
            .open_channel(OpenChannelRequest {
                network: Some(network.clone()),
                node_id,
                asset_amounts,
                fee_option: Some(fee_option),
            })
            .await?
            .into_inner();

        println!("✅ Channel opened successfully");
        Ok(response)
    }
}

/// Fee rate helper
pub enum FeeRate {
    Low,
    Medium,
    High,
}

impl FeeRate {
    fn to_fee_option(&self) -> FeeOption {
        match self {
            FeeRate::Low => FeeOption { option: Some(hydra_app::fee_option::Option::Low(())) },
            FeeRate::Medium => FeeOption { option: Some(hydra_app::fee_option::Option::Medium(())) },
            FeeRate::High => FeeOption { option: Some(hydra_app::fee_option::Option::High(())) },
        }
    }
}

Usage Example:

use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Connect to Hydra
    let mut client = HydraClient::connect("http://localhost:5001").await?;

    // Get networks
    let networks = client.get_networks().await?;
    println!("Networks: {:?}", networks);

    // Find Ethereum Sepolia
    let eth_network = networks
        .iter()
        .find(|n| n.protocol == Protocol::Evm as i32 && n.id == "11155111")
        .ok_or("Ethereum Sepolia not found")?;

    // Get balances
    let balances = client.get_balances(eth_network).await?;
    println!("Balances: {:?}", balances);

    // Open a channel
    let mut asset_amounts = HashMap::new();
    asset_amounts.insert(
        "0x0000000000000000000000000000000000000000".to_string(),
        SendAmount {
            amount: Some(hydra_app::send_amount::Amount::Exact(
                hydra_app::ExactAmount {
                    amount: Some(DecimalString {
                        value: "0.1".to_string(),
                    }),
                },
            )),
        },
    );

    client
        .open_channel(
            eth_network,
            "0xnode_id_here".to_string(),
            asset_amounts,
            FeeRate::Medium,
        )
        .await?;

    println!("✅ Channel opened");

    Ok(())
}

Cargo.toml dependencies:

[dependencies]
tonic = "0.11"
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1"
prost = "0.12"

[build-dependencies]
tonic-build = "0.11"

build.rs for code generation:

fn main() -> Result<(), Box<dyn std::error::Error>> {
    tonic_build::configure()
        .build_server(false)
        .compile(
            &[
                "proto/app.proto",
                "proto/wallet.proto",
                "proto/node.proto",
                "proto/models.proto",
            ],
            &["proto"],
        )?;
    Ok(())
}

See Also


Copyright © 2025