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
- Protobuf Encoding/Decoding
- Request Framing (TypeScript)
- Response Handling
- Event Streaming
- Error Handling
- Complete Client Implementation
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
toniccrate. 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(())
}