Connection

Establish a secure gRPC connection to NextBlock's API using Node.js and @grpc/grpc-js.

Prerequisites

Install the required dependencies:

npm install @grpc/grpc-js @grpc/proto-loader
npm install @solana/web3.js
npm install typescript @types/node  # For TypeScript

Generate the gRPC client from proto specs:

# Clone the proto repository
git clone https://github.com/nextblock-ag/nextblock-proto
# Follow the JavaScript/TypeScript generation instructions in the repo

Connection Setup

import * as grpc from '@grpc/grpc-js';
import * as protoLoader from '@grpc/proto-loader';
import { promisify } from 'util';

// Configuration interface
interface NextBlockConfig {
  endpoint: string;
  apiKey: string;
  useTLS: boolean;
  timeout: number;
  keepaliveTimeMs: number;
  keepaliveTimeoutMs: number;
}

// Default configuration
const defaultConfig: NextBlockConfig = {
  endpoint: 'fra.nextblock.io:443',
  apiKey: process.env.NEXTBLOCK_API_KEY || '',
  useTLS: true,
  timeout: 30000,
  keepaliveTimeMs: 60000,  // 60 seconds
  keepaliveTimeoutMs: 15000, // 15 seconds
};

// Authentication metadata interceptor
class AuthInterceptor {
  private apiKey: string;

  constructor(apiKey: string) {
    this.apiKey = apiKey;
  }

  // Add authorization header to all requests
  intercept(options: any, nextCall: any) {
    return new grpc.InterceptingCall(nextCall(options), {
      start: (metadata: grpc.Metadata, listener: any, next: any) => {
        metadata.set('authorization', this.apiKey);
        next(metadata, listener);
      },
    });
  }
}

// Create gRPC channel with authentication
export async function createNextBlockChannel(config: NextBlockConfig): Promise<grpc.Channel> {
  // Channel options for keepalive and performance
  const channelOptions: grpc.ChannelOptions = {
    'grpc.keepalive_time_ms': config.keepaliveTimeMs,
    'grpc.keepalive_timeout_ms': config.keepaliveTimeoutMs,
    'grpc.keepalive_permit_without_stream': 1,
    'grpc.http2.max_pings_without_data': 0,
    'grpc.http2.min_ping_interval_without_data_ms': 300000, // 5 minutes
  };

  // Create credentials
  const credentials = config.useTLS 
    ? grpc.credentials.createSsl()
    : grpc.credentials.createInsecure();

  // Create channel
  const channel = new grpc.Channel(config.endpoint, credentials, channelOptions);
  
  return channel;
}

// Create authenticated gRPC client
export async function createNextBlockClient(config: NextBlockConfig = defaultConfig) {
  // Validate configuration
  if (!config.apiKey) {
    throw new Error('API key is required');
  }

  // Create channel
  const channel = await createNextBlockChannel(config);

  // Create authentication interceptor
  const authInterceptor = new AuthInterceptor(config.apiKey);

  // Load proto definition (replace with your generated client)
  /* Example proto loading - replace with your actual proto
  const packageDefinition = protoLoader.loadSync('path/to/your/nextblock.proto', {
    keepCase: true,
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true,
  });

  const protoDescriptor = grpc.loadPackageDefinition(packageDefinition) as any;
  const ApiClient = protoDescriptor.nextblock.Api;

  // Create client with interceptor
  const client = new ApiClient(config.endpoint, credentials, {
    interceptors: [authInterceptor.intercept.bind(authInterceptor)],
    ...channelOptions,
  });
  */

  console.log(`Connected to NextBlock at ${config.endpoint}`);
  
  return {
    channel,
    // client, // Uncomment when you have the generated client
    config,
  };
}

// Connection manager with health checking
export class NextBlockConnectionManager {
  private config: NextBlockConfig;
  private channel?: grpc.Channel;
  private client?: any; // Replace with your generated client type
  private isConnected: boolean = false;

  constructor(config: NextBlockConfig = defaultConfig) {
    this.config = config;
  }

  // Establish connection
  async connect(): Promise<boolean> {
    try {
      const connection = await createNextBlockClient(this.config);
      this.channel = connection.channel;
      // this.client = connection.client;

      // Test the connection
      await this.healthCheck();
      this.isConnected = true;
      console.log(`Successfully connected to NextBlock at ${this.config.endpoint}`);
      return true;

    } catch (error) {
      console.error('Failed to connect to NextBlock:', error);
      this.isConnected = false;
      return false;
    }
  }

  // Health check
  async healthCheck(): Promise<boolean> {
    if (!this.channel) {
      return false;
    }

    return new Promise((resolve) => {
      // Check channel state
      const state = this.channel!.getConnectivityState(false);
      
      if (state === grpc.connectivityState.READY) {
        console.log('Connection health check passed');
        resolve(true);
      } else {
        console.log(`Connection state: ${grpc.connectivityState[state]}`);
        resolve(false);
      }
    });
  }

  // Disconnect
  async disconnect(): Promise<void> {
    if (this.channel) {
      this.channel.close();
      this.isConnected = false;
      console.log('Disconnected from NextBlock');
    }
  }

  // Getters
  get connected(): boolean {
    return this.isConnected;
  }

  get grpcClient(): any {
    return this.client;
  }
}

// Configuration from environment variables
export function configFromEnv(): NextBlockConfig {
  return {
    endpoint: process.env.NEXTBLOCK_ENDPOINT || 'fra.nextblock.io:443',
    apiKey: process.env.NEXTBLOCK_API_KEY || '',
    useTLS: process.env.NEXTBLOCK_USE_TLS !== 'false',
    timeout: parseInt(process.env.NEXTBLOCK_TIMEOUT || '30000'),
    keepaliveTimeMs: parseInt(process.env.NEXTBLOCK_KEEPALIVE_TIME_MS || '60000'),
    keepaliveTimeoutMs: parseInt(process.env.NEXTBLOCK_KEEPALIVE_TIMEOUT_MS || '15000'),
  };
}

Usage Examples

// Basic connection example
async function basicConnectionExample() {
  const config: NextBlockConfig = {
    endpoint: 'fra.nextblock.io:443',
    apiKey: '<your-api-key-here>',
    useTLS: true,
    timeout: 30000,
    keepaliveTimeMs: 60000,
    keepaliveTimeoutMs: 15000,
  };

  try {
    const connection = await createNextBlockClient(config);
    console.log('Connected to NextBlock!');

    // Your API calls would go here
    // const response = await connection.client.ping({});
    
    // Clean up
    connection.channel.close();
    
  } catch (error) {
    console.error('Connection failed:', error);
  }
}

// Connection manager example (recommended)
async function connectionManagerExample() {
  const config = configFromEnv();
  const manager = new NextBlockConnectionManager(config);

  try {
    const connected = await manager.connect();
    
    if (connected) {
      console.log('Connection manager established connection');
      
      // Use manager.grpcClient for API calls
      // const response = await manager.grpcClient.ping({});
      
      // Keep connection alive for your application
      await new Promise(resolve => setTimeout(resolve, 5000));
      
    } else {
      console.error('Failed to establish connection');
    }
    
  } finally {
    await manager.disconnect();
  }
}

// Advanced connection with retry logic
class EnhancedConnectionManager extends NextBlockConnectionManager {
  private maxRetries: number = 3;
  private retryDelay: number = 5000; // 5 seconds

  async connectWithRetry(): Promise<boolean> {
    for (let attempt = 1; attempt <= this.maxRetries; attempt++) {
      console.log(`Connection attempt ${attempt}/${this.maxRetries}`);
      
      const success = await this.connect();
      if (success) {
        return true;
      }

      if (attempt < this.maxRetries) {
        console.log(`Retrying in ${this.retryDelay}ms...`);
        await new Promise(resolve => setTimeout(resolve, this.retryDelay));
        this.retryDelay *= 2; // Exponential backoff
      }
    }

    console.error('All connection attempts failed');
    return false;
  }

  async callWithRetry<T>(
    fn: () => Promise<T>,
    maxRetries: number = 3
  ): Promise<T> {
    let lastError: Error;

    for (let attempt = 1; attempt <= maxRetries; attempt++) {
      try {
        return await fn();
      } catch (error) {
        lastError = error as Error;
        
        // Don't retry on authentication errors
        if (error && (error as any).code === grpc.status.UNAUTHENTICATED) {
          throw error;
        }

        if (attempt < maxRetries) {
          const delay = Math.min(1000 * Math.pow(2, attempt - 1), 30000);
          console.log(`Attempt ${attempt} failed, retrying in ${delay}ms...`);
          await new Promise(resolve => setTimeout(resolve, delay));
        }
      }
    }

    throw lastError!;
  }
}

// Connection pool for high-throughput applications
class ConnectionPool {
  private connections: NextBlockConnectionManager[] = [];
  private currentIndex: number = 0;
  private config: NextBlockConfig;
  private poolSize: number;

  constructor(config: NextBlockConfig, poolSize: number = 5) {
    this.config = config;
    this.poolSize = poolSize;
  }

  async initialize(): Promise<void> {
    console.log(`Initializing connection pool with ${this.poolSize} connections...`);
    
    const connectionPromises = Array.from({ length: this.poolSize }, async (_, i) => {
      const manager = new NextBlockConnectionManager(this.config);
      const connected = await manager.connect();
      
      if (connected) {
        this.connections.push(manager);
        console.log(`Connection ${i + 1} established`);
      } else {
        console.warn(`Failed to create connection ${i + 1}`);
      }
    });

    await Promise.all(connectionPromises);
    console.log(`Connection pool initialized with ${this.connections.length} connections`);
  }

  getConnection(): NextBlockConnectionManager {
    if (this.connections.length === 0) {
      throw new Error('No available connections in pool');
    }

    const connection = this.connections[this.currentIndex];
    this.currentIndex = (this.currentIndex + 1) % this.connections.length;
    return connection;
  }

  async closeAll(): Promise<void> {
    await Promise.all(this.connections.map(conn => conn.disconnect()));
    this.connections = [];
    console.log('All connections closed');
  }
}

// Run examples
async function main() {
  console.log('NextBlock Connection Examples');
  
  // Example 1: Basic connection
  console.log('\n1. Basic Connection:');
  await basicConnectionExample();
  
  // Example 2: Connection manager
  console.log('\n2. Connection Manager:');
  await connectionManagerExample();
  
  // Example 3: Enhanced connection with retry
  console.log('\n3. Enhanced Connection:');
  const enhancedManager = new EnhancedConnectionManager(configFromEnv());
  const connected = await enhancedManager.connectWithRetry();
  if (connected) {
    console.log('Enhanced connection successful');
    await enhancedManager.disconnect();
  }
}

// Export for use in other modules
export {
  NextBlockConfig,
  NextBlockConnectionManager,
  EnhancedConnectionManager,
  ConnectionPool,
  configFromEnv,
};

if (require.main === module) {
  main().catch(console.error);
}

Available Endpoints

  • Frankfurt: fra.nextblock.io:443 (Europe)

  • New York: ny.nextblock.io:443 (US East)

  • Direct NY: direct-ny.nextblock.io:443 (US East, optimized routing)

Best Practices

  1. Use TypeScript: Leverage type safety for better development experience

  2. Enable TLS: Always use secure connections in production

  3. Implement keepalive: Configure appropriate keepalive settings for persistent connections

  4. Handle errors gracefully: Use retry logic with exponential backoff

  5. Validate configuration: Check all required settings before connecting

  6. Use connection pooling: For high-throughput applications

  7. Monitor connection health: Implement regular health checks

  8. Environment variables: Store sensitive configuration securely

  9. Close connections properly: Always close connections when done

  10. Use interceptors: Implement authentication and logging via gRPC interceptors

Last updated