UNPKG

@btc-stamps/tx-builder

Version:

Transaction builder for Bitcoin Stamps and SRC-20 tokens with advanced UTXO selection

1,316 lines (1,160 loc) 40.7 kB
/** * ElectrumX Connection Pool Manager * Manages multiple ElectrumX connections with load balancing and failover */ import type { Network } from 'bitcoinjs-lib'; import { clearIntervalCompat, clearTimeoutCompat, setIntervalCompat, setTimeoutCompat, } from '../utils/timer-utils.ts'; import type { AddressHistory, AddressHistoryOptions, Balance, ElectrumXOptions as _ElectrumXOptions, Transaction, UTXO, } from '../interfaces/provider.interface.ts'; import { getElectrumXEndpoints } from '../config/electrumx-config.ts'; import { ServerPerformanceMonitor } from '../monitoring/server-metrics.ts'; import { ElectrumXProvider } from './electrumx-provider.ts'; import process from 'node:process'; export interface ElectrumXServer { host: string; port: number; protocol?: 'tcp' | 'ssl' | 'ws' | 'wss'; weight?: number; // Load balancing weight (higher = more requests) region?: string; // Optional region identifier timeout?: number; } export interface ConnectionPoolOptions { network: Network; servers: ElectrumXServer[]; maxConnectionsPerServer?: number; minConnectionsPerServer?: number; healthCheckInterval?: number; heartbeatInterval?: number; connectionTimeout?: number; requestTimeout?: number; retries?: number; retryDelay?: number; maxRetryDelay?: number; backoffMultiplier?: number; loadBalanceStrategy?: | 'round-robin' | 'weighted' | 'least-connections' | 'health-based'; failoverThreshold?: number; // Failed requests before marking unhealthy circuitBreakerThreshold?: number; // Failures before circuit opens circuitBreakerTimeout?: number; // Time before attempting to close circuit recoveryTimeout?: number; // Time before retrying failed servers maxPoolSize?: number; // Maximum total connections across all servers enableDynamicScaling?: boolean; // Enable automatic pool size adjustment } enum CircuitBreakerState { CLOSED = 'closed', OPEN = 'open', HALF_OPEN = 'half_open', } interface ServerHealth { healthy: boolean; consecutiveFailures: number; lastFailTime: number; lastSuccessTime: number; lastHeartbeatTime: number; totalRequests: number; successfulRequests: number; averageResponseTime: number; activeConnections: number; healthScore: number; // 0-100 composite health score circuitBreakerState: CircuitBreakerState; circuitBreakerOpenTime?: number; lastCircuitBreakerResetTime?: number; } interface ActiveConnection { provider: ElectrumXProvider; inUse: boolean; createdAt: number; lastUsed: number; requestCount: number; lastHeartbeat: number; consecutiveFailures: number; healthy: boolean; responseTimeHistory: number[]; // Track last 10 response times } /** * ElectrumX Connection Pool with advanced load balancing and health monitoring */ export class ElectrumXConnectionPool { private options: Required<ConnectionPoolOptions>; private connections: Map<string, ActiveConnection[]> = new Map<string, ActiveConnection[]>(); private serverHealth: Map<string, ServerHealth> = new Map<string, ServerHealth>(); private performanceMonitor: ServerPerformanceMonitor = new ServerPerformanceMonitor(); private currentServerIndex: number = 0; private healthCheckTimer: number | null = null; private heartbeatTimer: number | null = null; private connectionWaiters = new Map< string, Array< { resolve: (conn: ActiveConnection) => void; reject: (error: Error) => void; timeout: number; } > >(); private totalConnectionCount: number = 0; constructor(options: ConnectionPoolOptions) { this.options = { maxConnectionsPerServer: parseInt(process.env.ELECTRUMX_POOL_SIZE || '3'), minConnectionsPerServer: 1, healthCheckInterval: parseInt( process.env.ELECTRUMX_HEALTH_CHECK_INTERVAL || '30000', ), heartbeatInterval: 15000, // 15 seconds connectionTimeout: parseInt( process.env.ELECTRUMX_CONNECTION_TIMEOUT || '10000', ), requestTimeout: 30000, retries: parseInt(process.env.ELECTRUMX_MAX_RETRIES || '3'), retryDelay: 1000, maxRetryDelay: 10000, backoffMultiplier: 2, loadBalanceStrategy: 'health-based', failoverThreshold: 3, circuitBreakerThreshold: parseInt( process.env.ELECTRUMX_CIRCUIT_BREAKER_THRESHOLD || '5', ), circuitBreakerTimeout: 30000, // 30 seconds recoveryTimeout: 60000, // 1 minute maxPoolSize: 50, enableDynamicScaling: true, ...options, }; // Initialize performance monitoring for all servers for (const server of this.options.servers) { const serverKey = this.getServerKey(server); this.performanceMonitor.initializeServer(serverKey); } this.initializeServerHealth(); this.startHealthChecking(); this.startHeartbeat(); } /** * Get UTXOs using the best available connection */ async getUTXOs(address: string): Promise<UTXO[]> { return await this.executeWithLoadBalancing( 'getUTXOs', async (provider) => await provider.getUTXOs(address), ); } /** * Get balance using the best available connection */ async getBalance(address: string): Promise<Balance> { return await this.executeWithLoadBalancing( 'getBalance', async (provider) => await provider.getBalance(address), ); } /** * Get transaction using the best available connection */ async getTransaction(txid: string): Promise<Transaction> { return await this.executeWithLoadBalancing( 'getTransaction', async (provider) => await provider.getTransaction(txid), ); } /** * Broadcast transaction using the best available connection */ async broadcastTransaction(hexTx: string): Promise<string> { return await this.executeWithLoadBalancing( 'broadcastTransaction', async (provider) => await provider.broadcastTransaction(hexTx), ); } /** * Get fee rate using the best available connection */ async getFeeRate(priority?: 'low' | 'medium' | 'high'): Promise<number> { return await this.executeWithLoadBalancing( 'getFeeRate', async (provider) => await provider.getFeeRate(priority), ); } /** * Get block height using the best available connection */ async getBlockHeight(): Promise<number> { return await this.executeWithLoadBalancing( 'getBlockHeight', async (provider) => await provider.getBlockHeight(), ); } /** * Get address transaction history using the best available connection */ async getAddressHistory( address: string, options?: AddressHistoryOptions, ): Promise<AddressHistory[]> { return await this.executeWithLoadBalancing( 'getAddressHistory', async (provider) => await provider.getAddressHistory(address, options), ); } /** * Execute operation with load balancing, circuit breaker, and exponential backoff */ private async executeWithLoadBalancing<T>( operationName: string, operation: (provider: ElectrumXProvider) => Promise<T>, ): Promise<T> { const availableServers = this.getAvailableServers(); if (availableServers.length === 0) { throw new Error( 'No available ElectrumX servers (all unhealthy or circuit breaker open)', ); } let lastError: Error | null = null; let attempts = 0; const maxAttempts = Math.min( availableServers.length * 2, this.options.retries * 2, ); while (attempts < maxAttempts) { const server = this.selectServer(availableServers); const serverKey = this.getServerKey(server); let connection: ActiveConnection | null = null; try { // Check circuit breaker before attempting operation if (!this.isServerAvailable(serverKey)) { attempts++; continue; } connection = await this.getOrCreateConnection(server); const startTime = Date.now(); // Execute operation with timeout const result = await Promise.race([ operation(connection.provider), this.createTimeoutPromise( this.options.requestTimeout, `${operationName} timeout`, ), ]) as T; // Update success metrics const responseTime = Date.now() - startTime; this.updateConnectionSuccess(connection, responseTime); this.updateServerHealth(serverKey, true, responseTime); // Record performance metrics this.performanceMonitor.recordSuccess(serverKey, responseTime); this.releaseConnection(serverKey, connection); return result; } catch (error) { attempts++; lastError = error as Error; // Update failure metrics if (connection) { this.updateConnectionFailure(connection); this.releaseConnection(serverKey, connection); } this.updateServerHealth(serverKey, false); // Record performance metrics this.performanceMonitor.recordFailure(serverKey, lastError?.message); // Apply exponential backoff before retry if (attempts < maxAttempts) { const delay = Math.min( this.options.retryDelay * Math.pow(this.options.backoffMultiplier, attempts - 1), this.options.maxRetryDelay, ); await this.sleep(delay); } } } throw lastError || new Error(`All ElectrumX servers failed for operation: ${operationName}`); } /** * Create a timeout promise */ private createTimeoutPromise<T>(ms: number, message: string): Promise<T> { return new Promise((_, reject) => { setTimeout(() => reject(new Error(message)), ms); }); } /** * Get or create a connection to the specified server with improved queueing */ private async getOrCreateConnection( server: ElectrumXServer, ): Promise<ActiveConnection> { const serverKey = this.getServerKey(server); const connections = this.connections.get(serverKey) || []; // Find available healthy connection const availableConnection = connections.find((conn) => !conn.inUse && conn.healthy); if (availableConnection) { availableConnection.inUse = true; availableConnection.lastUsed = Date.now(); availableConnection.requestCount++; return Promise.resolve(availableConnection); } // Create new connection if under limits if ( connections.length < this.options.maxConnectionsPerServer && this.totalConnectionCount < this.options.maxPoolSize ) { try { const connection = await this.createNewConnection(server, serverKey); connections.push(connection); this.connections.set(serverKey, connections); this.totalConnectionCount++; // Update health metrics const health = this.serverHealth.get(serverKey)!; health.activeConnections = connections.length; return connection; } catch (error) { // If connection creation fails, try to wait for existing connection console.warn(`Failed to create new connection to ${serverKey}:`, error); } } // Wait for available connection using proper queue return this.waitForAvailableConnection(serverKey); } /** * Create a new connection to the server */ private async createNewConnection( server: ElectrumXServer, _serverKey: string, ): Promise<ActiveConnection> { const provider = new ElectrumXProvider({ host: server.host, port: server.port, network: this.options.network, protocol: server.protocol || 'wss', timeout: server.timeout || this.options.connectionTimeout, retries: 1, // Pool handles retries }); // Test the connection await provider.isConnected(); const connection: ActiveConnection = { provider, inUse: true, createdAt: Date.now(), lastUsed: Date.now(), requestCount: 1, lastHeartbeat: Date.now(), consecutiveFailures: 0, healthy: true, responseTimeHistory: [], }; return connection; } /** * Wait for an available connection using proper queueing */ private waitForAvailableConnection( serverKey: string, ): Promise<ActiveConnection> { return new Promise((resolve, reject) => { const timeout = setTimeoutCompat(() => { this.removeWaiter(serverKey, resolve, reject); reject(new Error(`Connection pool exhausted for server: ${serverKey}`)); }, this.options.connectionTimeout); // Add to queue const waiters = this.connectionWaiters.get(serverKey) || []; waiters.push({ resolve, reject, timeout: timeout as unknown as number }); this.connectionWaiters.set(serverKey, waiters); }); } /** * Remove waiter from queue */ private removeWaiter( serverKey: string, resolve: (value: ActiveConnection) => void, reject: (reason: Error) => void, ): void { const waiters = this.connectionWaiters.get(serverKey) || []; const index = waiters.findIndex((w) => w.resolve === resolve && w.reject === reject); if (index > -1) { const waiter = waiters[index]; if (waiter?.timeout) { clearTimeoutCompat(waiter.timeout); } waiters.splice(index, 1); this.connectionWaiters.set(serverKey, waiters); } } /** * Release connection back to pool and notify waiters */ private releaseConnection( serverKey: string, connection: ActiveConnection, ): void { connection.inUse = false; // Check if there are waiters for this server const waiters = this.connectionWaiters.get(serverKey) || []; if (waiters.length > 0 && connection.healthy) { const waiter = waiters.shift()!; clearTimeout(waiter.timeout as unknown as number); // Mark connection as in use and resolve connection.inUse = true; connection.lastUsed = Date.now(); connection.requestCount++; waiter.resolve(connection); this.connectionWaiters.set(serverKey, waiters); } } /** * Update connection success metrics */ private updateConnectionSuccess( connection: ActiveConnection, responseTime: number, ): void { connection.consecutiveFailures = 0; connection.healthy = true; connection.lastHeartbeat = Date.now(); // Update response time history (keep last 10) connection.responseTimeHistory.push(responseTime); if (connection.responseTimeHistory.length > 10) { connection.responseTimeHistory.shift(); } } /** * Update connection failure metrics */ private updateConnectionFailure(connection: ActiveConnection): void { connection.consecutiveFailures++; // Mark connection as unhealthy after 3 consecutive failures if (connection.consecutiveFailures >= 3) { connection.healthy = false; } } /** * Select best server based on load balancing strategy */ private selectServer(servers: ElectrumXServer[]): ElectrumXServer { switch (this.options.loadBalanceStrategy) { case 'round-robin': return this.selectRoundRobin(servers); case 'weighted': return this.selectWeighted(servers); case 'least-connections': return this.selectLeastConnections(servers); case 'health-based': default: return this.selectHealthBased(servers); } } /** * Round-robin server selection */ private selectRoundRobin(servers: ElectrumXServer[]): ElectrumXServer { if (servers.length === 0) { throw new Error('No servers available for round-robin selection'); } if (servers.length === 0) { throw new Error('No servers available for round-robin selection'); } const server = servers[this.currentServerIndex % servers.length]; this.currentServerIndex++; return server as ElectrumXServer; } /** * Weighted server selection */ private selectWeighted(servers: ElectrumXServer[]): ElectrumXServer { if (servers.length === 0) { throw new Error('No servers available for weighted selection'); } const totalWeight = servers.reduce( (sum, server) => sum + (server.weight || 1), 0, ); let random = Math.random() * totalWeight; for (const server of servers) { random -= server.weight || 1; if (random <= 0) { return server; } } return servers[0] as ElectrumXServer; } /** * Least connections server selection */ private selectLeastConnections(servers: ElectrumXServer[]): ElectrumXServer { if (servers.length === 0) { throw new Error('No servers available for least connections selection'); } if (servers.length === 0) { throw new Error('No servers available for least connections selection'); } return servers.reduce((best, server) => { const serverKey = this.getServerKey(server); const bestKey = this.getServerKey(best); const serverHealth = this.serverHealth.get(serverKey); const bestHealth = this.serverHealth.get(bestKey); if (!serverHealth || !bestHealth) { return best; } return serverHealth.activeConnections < bestHealth.activeConnections ? server : best; }, servers[0] as ElectrumXServer); } /** * Health-based server selection with enhanced performance metrics */ private selectHealthBased(servers: ElectrumXServer[]): ElectrumXServer { // Score servers based on comprehensive performance metrics const scoredServers = servers.map((server) => { const serverKey = this.getServerKey(server); const health = this.serverHealth.get(serverKey); if (!health) { console.warn(`No health metrics for server: ${serverKey}`); return { server, score: 0, metrics: null }; } const performanceMetrics = this.performanceMonitor.getMetrics(serverKey); // Use performance monitor metrics if available, fallback to health metrics let overallScore = health.healthScore; if (performanceMetrics) { overallScore = performanceMetrics.overallScore; // Update connection metrics in performance monitor this.performanceMonitor.updateConnectionMetrics( serverKey, health.activeConnections, health.activeConnections, 0, // Connection failures tracked separately ); } // Apply server weight multiplier const weightedScore = overallScore * (server.weight || 1); return { server, score: weightedScore, metrics: performanceMetrics }; }); // Sort by weighted score (higher is better) scoredServers.sort((a, b) => b.score - a.score); return scoredServers[0]?.server || servers[0]!; } /** * Get healthy servers (circuit breaker aware) */ private getHealthyServers(): ElectrumXServer[] { return this.options.servers.filter((server) => { const serverKey = this.getServerKey(server); const health = this.serverHealth.get(serverKey); return health?.healthy === true && health?.circuitBreakerState === CircuitBreakerState.CLOSED; }); } /** * Get available servers (includes healthy servers and half-open circuit breakers) */ private getAvailableServers(): ElectrumXServer[] { return this.options.servers.filter((server) => { const serverKey = this.getServerKey(server); return this.isServerAvailable(serverKey); }); } /** * Check if server is available (healthy or circuit breaker allows test) */ private isServerAvailable(serverKey: string): boolean { const health = this.serverHealth.get(serverKey); if (!health) return false; switch (health.circuitBreakerState) { case CircuitBreakerState.CLOSED: return health.healthy; case CircuitBreakerState.HALF_OPEN: return true; // Allow one test request case CircuitBreakerState.OPEN: // Check if circuit breaker should transition to half-open if ( health.circuitBreakerOpenTime && Date.now() - health.circuitBreakerOpenTime > this.options.circuitBreakerTimeout ) { health.circuitBreakerState = CircuitBreakerState.HALF_OPEN; return true; } return false; default: return false; } } /** * Initialize server health tracking with circuit breaker support */ private initializeServerHealth(): void { for (const server of this.options.servers) { const serverKey = this.getServerKey(server); this.serverHealth.set(serverKey, { healthy: true, consecutiveFailures: 0, lastFailTime: 0, lastSuccessTime: Date.now(), lastHeartbeatTime: Date.now(), totalRequests: 0, successfulRequests: 0, averageResponseTime: 0, activeConnections: 0, healthScore: 100, // Start with perfect health score circuitBreakerState: CircuitBreakerState.CLOSED, }); } } /** * Update server health metrics with circuit breaker logic */ private updateServerHealth( serverKey: string, success: boolean, responseTime?: number, ): void { const health = this.serverHealth.get(serverKey); if (!health) { console.warn(`No server health found for key: ${serverKey}`); return; } health.totalRequests++; if (success) { health.successfulRequests++; health.consecutiveFailures = 0; health.lastSuccessTime = Date.now(); health.lastHeartbeatTime = Date.now(); if (responseTime !== undefined) { // Update moving average response time const alpha = 0.1; // Smoothing factor health.averageResponseTime = health.averageResponseTime === 0 ? responseTime : alpha * responseTime + (1 - alpha) * health.averageResponseTime; } // Update health score (composite metric) this.updateHealthScore(health); // Handle circuit breaker state transitions on success if (health.circuitBreakerState === CircuitBreakerState.HALF_OPEN) { // Successful request in half-open state - close the circuit health.circuitBreakerState = CircuitBreakerState.CLOSED; health.lastCircuitBreakerResetTime = Date.now(); this.performanceMonitor.recordCircuitBreakerTrip(serverKey, 'closed'); console.log( `ElectrumX server ${serverKey} circuit breaker closed - server recovered`, ); } // Mark as healthy if it was unhealthy if (!health.healthy) { health.healthy = true; console.log(`ElectrumX server ${serverKey} marked as healthy`); } } else { health.consecutiveFailures++; health.lastFailTime = Date.now(); // Update health score this.updateHealthScore(health); // Handle circuit breaker state transitions on failure if (health.circuitBreakerState === CircuitBreakerState.HALF_OPEN) { // Failed request in half-open state - open the circuit again health.circuitBreakerState = CircuitBreakerState.OPEN; health.circuitBreakerOpenTime = Date.now(); this.performanceMonitor.recordCircuitBreakerTrip(serverKey, 'open'); console.warn( `ElectrumX server ${serverKey} circuit breaker reopened after failed test`, ); } else if ( health.circuitBreakerState === CircuitBreakerState.CLOSED && health.consecutiveFailures >= this.options.circuitBreakerThreshold ) { // Too many failures - open the circuit health.circuitBreakerState = CircuitBreakerState.OPEN; health.circuitBreakerOpenTime = Date.now(); this.performanceMonitor.recordCircuitBreakerTrip(serverKey, 'open'); console.warn( `ElectrumX server ${serverKey} circuit breaker opened after ${health.consecutiveFailures} consecutive failures`, ); } // Mark as unhealthy if threshold exceeded if ( health.consecutiveFailures >= this.options.failoverThreshold && health.healthy ) { health.healthy = false; console.warn( `ElectrumX server ${serverKey} marked as unhealthy after ${health.consecutiveFailures} consecutive failures`, ); } } } /** * Update composite health score (0-100) */ private updateHealthScore(health: ServerHealth): void { const successRate = health.totalRequests > 0 ? (health.successfulRequests / health.totalRequests) * 100 : 100; const responseTimeFactor = health.averageResponseTime > 0 ? Math.max(0, 100 - (health.averageResponseTime / 1000) * 10) // Penalize high response times : 100; const recentFailurePenalty = Math.max( 0, 100 - (health.consecutiveFailures * 20), ); // Composite score with weights health.healthScore = Math.round( successRate * 0.5 + responseTimeFactor * 0.3 + recentFailurePenalty * 0.2, ); } /** * Start periodic health checking */ private startHealthChecking(): void { this.healthCheckTimer = setIntervalCompat(async () => { await this.performHealthChecks(); this.cleanupIdleConnections(); this.adjustPoolSize(); }, this.options.healthCheckInterval) as number; } /** * Start heartbeat monitoring */ private startHeartbeat(): void { this.heartbeatTimer = setIntervalCompat(async () => { await this.performHeartbeats(); }, this.options.heartbeatInterval) as number; } /** * Perform health checks on all servers */ private async performHealthChecks(): Promise<void> { const promises = this.options.servers.map(async (server) => { const serverKey = this.getServerKey(server); const health = this.serverHealth.get(serverKey)!; // Try to recover unhealthy servers or test circuit breaker recovery const shouldTestRecovery = (!health.healthy && Date.now() - health.lastFailTime > this.options.recoveryTimeout) || (health.circuitBreakerState === CircuitBreakerState.HALF_OPEN); if (shouldTestRecovery) { try { const connection = await this.getOrCreateConnection(server); const startTime = Date.now(); await connection.provider.getBlockHeight(); // Simple health check const responseTime = Date.now() - startTime; this.updateServerHealth(serverKey, true, responseTime); this.updateConnectionSuccess(connection, responseTime); this.releaseConnection(serverKey, connection); console.log(`ElectrumX server ${serverKey} health check passed`); } catch (error) { this.updateServerHealth(serverKey, false); console.warn( `ElectrumX server ${serverKey} health check failed:`, error, ); } } }); await Promise.allSettled(promises); } /** * Perform heartbeat checks on active connections */ private async performHeartbeats(): Promise<void> { const now = Date.now(); const heartbeatPromises: Promise<void>[] = []; for (const [serverKey, connections] of this.connections) { const health = this.serverHealth.get(serverKey); if (!health) continue; for (const connection of connections) { // Skip connections that are in use or recently used if (connection.inUse || now - connection.lastUsed < 5000) continue; // Check if heartbeat is needed if (now - connection.lastHeartbeat > this.options.heartbeatInterval) { heartbeatPromises.push( this.performConnectionHeartbeat(serverKey, connection), ); } } } await Promise.allSettled(heartbeatPromises); } /** * Perform heartbeat on a specific connection */ private async performConnectionHeartbeat( serverKey: string, connection: ActiveConnection, ): Promise<void> { try { const startTime = Date.now(); await connection.provider.isConnected(); const responseTime = Date.now() - startTime; connection.lastHeartbeat = Date.now(); this.updateConnectionSuccess(connection, responseTime); this.updateServerHealth(serverKey, true, responseTime); } catch (error) { console.warn(`Heartbeat failed for connection to ${serverKey}:`, error); this.updateConnectionFailure(connection); this.updateServerHealth(serverKey, false); // Mark connection for removal if it's consistently failing if (connection.consecutiveFailures >= 5) { await this.removeConnection(serverKey, connection); } } } /** * Remove a specific connection from the pool */ private async removeConnection( serverKey: string, connection: ActiveConnection, ): Promise<void> { const connections = this.connections.get(serverKey) || []; const index = connections.indexOf(connection); if (index > -1) { // Disconnect the connection try { await connection.provider.disconnect(); } catch (error) { console.warn(`Error disconnecting connection to ${serverKey}:`, error); } // Remove from pool connections.splice(index, 1); this.connections.set(serverKey, connections); this.totalConnectionCount--; // Update health metrics const health = this.serverHealth.get(serverKey); if (health) { health.activeConnections = connections.length; } console.log(`Removed unhealthy connection to ${serverKey}`); } } /** * Cleanup idle and unhealthy connections */ private cleanupIdleConnections(): void { const maxIdleTime = 5 * 60 * 1000; // 5 minutes const now = Date.now(); for (const [serverKey, connections] of this.connections) { const activeConnections: ActiveConnection[] = []; const connectionsToRemove: ActiveConnection[] = []; for (const conn of connections) { const shouldRemove = ( // Remove idle connections (!conn.inUse && now - conn.lastUsed > maxIdleTime) || // Remove unhealthy connections that haven't been used recently (!conn.healthy && !conn.inUse && now - conn.lastUsed > 30000) || // Remove connections with too many consecutive failures (conn.consecutiveFailures >= 10) ); if (shouldRemove) { connectionsToRemove.push(conn); } else { activeConnections.push(conn); } } // Disconnect and remove connections for (const conn of connectionsToRemove) { try { conn.provider.disconnect(); } catch (error) { console.warn( `Error disconnecting connection to ${serverKey}:`, error, ); } this.totalConnectionCount--; } this.connections.set(serverKey, activeConnections); // Update health metrics const health = this.serverHealth.get(serverKey); if (health) { health.activeConnections = activeConnections.length; } if (connectionsToRemove.length > 0) { console.log( `Cleaned up ${connectionsToRemove.length} connections for ${serverKey}`, ); } } } /** * Adjust pool size based on load and performance */ private adjustPoolSize(): void { if (!this.options.enableDynamicScaling) return; for (const [serverKey, connections] of this.connections) { const health = this.serverHealth.get(serverKey); if (!health || !health.healthy) continue; const server = this.options.servers.find((s) => this.getServerKey(s) === serverKey); if (!server) continue; const activeConnections = connections.filter((c) => c.inUse).length; const totalConnections = connections.length; const utilizationRate = totalConnections > 0 ? activeConnections / totalConnections : 0; // Scale up if utilization is high and we're under limits if ( utilizationRate > 0.8 && totalConnections < this.options.maxConnectionsPerServer && this.totalConnectionCount < this.options.maxPoolSize ) { // Create additional connection proactively this.createNewConnection(server, serverKey) .then((connection) => { connections.push(connection); this.connections.set(serverKey, connections); this.totalConnectionCount++; health.activeConnections = connections.length; console.log( `Scaled up connections for ${serverKey} to ${connections.length}`, ); }) .catch((error) => { console.warn( `Failed to scale up connections for ${serverKey}:`, error, ); }); } // Scale down if utilization is consistently low if ( utilizationRate < 0.2 && totalConnections > this.options.minConnectionsPerServer ) { const idleConnection = connections.find((c) => !c.inUse && Date.now() - c.lastUsed > 60000); if (idleConnection) { this.removeConnection(serverKey, idleConnection) .then(() => { console.log( `Scaled down connections for ${serverKey} to ${connections.length - 1}`, ); }) .catch((error) => { console.warn( `Failed to scale down connections for ${serverKey}:`, error, ); }); } } } } /** * Get server key for mapping */ private getServerKey(server: ElectrumXServer): string { return `${server.host}:${server.port}:${server.protocol || 'wss'}`; } /** * Sleep for specified milliseconds */ private sleep(ms: number): Promise<void> { return new Promise((resolve) => setTimeout(resolve, ms)); } /** * Get comprehensive pool statistics */ getStats(): { servers: Array<{ server: string; healthy: boolean; activeConnections: number; totalRequests: number; successRate: number; averageResponseTime: number; consecutiveFailures: number; healthScore: number; circuitBreakerState: string; lastHeartbeat: Date | null; connectionsInUse: number; }>; totalConnections: number; totalActiveConnections: number; averageHealthScore: number; circuitBreakersOpen: number; } { const servers = this.options.servers.map((server) => { const serverKey = this.getServerKey(server); const health = this.serverHealth.get(serverKey)!; const connections = this.connections.get(serverKey) || []; const connectionsInUse = connections.filter((c) => c.inUse).length; return { server: serverKey, healthy: health.healthy, activeConnections: connections.length, connectionsInUse, totalRequests: health.totalRequests, successRate: health.totalRequests > 0 ? health.successfulRequests / health.totalRequests : 0, averageResponseTime: health.averageResponseTime, consecutiveFailures: health.consecutiveFailures, healthScore: health.healthScore, circuitBreakerState: health.circuitBreakerState, lastHeartbeat: health.lastHeartbeatTime > 0 ? new Date(health.lastHeartbeatTime) : null, }; }); const totalConnections = this.totalConnectionCount; const totalActiveConnections = Array.from(this.connections.values()).reduce( (total, conns) => total + conns.filter((c) => c.inUse).length, 0, ); const averageHealthScore = servers.length > 0 ? servers.reduce((sum, s) => sum + s.healthScore, 0) / servers.length : 0; const circuitBreakersOpen = servers.filter((s) => s.circuitBreakerState === CircuitBreakerState.OPEN) .length; // Get performance summary from monitor const _performanceSummary = this.performanceMonitor.getPerformanceSummary(); // Enhance server stats with performance metrics const enhancedServers = servers.map((server) => { const performanceMetrics = this.performanceMonitor.getMetrics( server.server, ); if (performanceMetrics) { return { ...server, performanceScore: performanceMetrics.performanceScore, reliabilityScore: performanceMetrics.reliabilityScore, overallScore: performanceMetrics.overallScore, p95ResponseTime: performanceMetrics.p95ResponseTime, p99ResponseTime: performanceMetrics.p99ResponseTime, uptime: performanceMetrics.uptime, circuitBreakerTrips: performanceMetrics.circuitBreakerTrips, }; } return server; }); return { servers: enhancedServers, totalConnections, totalActiveConnections, averageHealthScore, circuitBreakersOpen, }; } /** * Get detailed performance metrics for a specific server */ getServerPerformanceMetrics( serverKey: string, ): import('../monitoring/server-metrics.ts').ServerMetrics | null { return this.performanceMonitor.getMetrics(serverKey); } /** * Get performance history for a specific server */ getServerPerformanceHistory( serverKey: string, ): import('../monitoring/server-metrics.ts').ServerPerformanceHistory | null { return this.performanceMonitor.getPerformanceHistory(serverKey); } /** * Get servers ranked by performance */ getRankedServersByPerformance(): Array< { serverId: string; metrics: import('../monitoring/server-metrics.ts').ServerMetrics } > { return this.performanceMonitor.getRankedServers(); } /** * Get only healthy servers based on performance criteria */ getHealthyServersByPerformance( minScore = 70, ): Array<{ serverId: string; metrics: import('../monitoring/server-metrics.ts').ServerMetrics }> { return this.performanceMonitor.getHealthyServers(minScore); } /** * Shutdown the connection pool */ async shutdown(): Promise<void> { // Stop timers if (this.healthCheckTimer) { clearIntervalCompat(this.healthCheckTimer); this.healthCheckTimer = null; } if (this.heartbeatTimer) { clearIntervalCompat(this.heartbeatTimer); this.heartbeatTimer = null; } // Clear all waiters for (const [_serverKey, waiters] of this.connectionWaiters) { for (const waiter of waiters) { clearTimeoutCompat(waiter.timeout); waiter.reject(new Error('Connection pool shutting down')); } } this.connectionWaiters.clear(); // Close all connections const disconnectPromises: Promise<void>[] = []; for (const connections of this.connections.values()) { for (const connection of connections) { disconnectPromises.push(connection.provider.disconnect()); } } await Promise.allSettled(disconnectPromises); this.connections.clear(); this.serverHealth.clear(); this.totalConnectionCount = 0; } } /** * Create ElectrumX connection pool with servers from configuration */ export function createElectrumXPool( network: Network, customServers?: ElectrumXServer[], options?: Partial<ConnectionPoolOptions>, ): ElectrumXConnectionPool { let servers: ElectrumXServer[]; if (customServers) { servers = customServers; } else { // Get network name for configuration lookup const networkName = getNetworkNameFromNetwork(network); // Get endpoints from centralized config const endpoints = getElectrumXEndpoints(networkName); // Convert to ElectrumXServer format servers = endpoints.map((endpoint, index) => ({ host: endpoint.host, port: endpoint.port, protocol: endpoint.protocol, weight: Math.max(1, 4 - (endpoint.priority || (index + 1))), timeout: endpoint.timeout, })); } return new ElectrumXConnectionPool({ network, servers, ...options, }); } /** * Helper function to get network name from Network object */ function getNetworkNameFromNetwork(network: Network): string { if (network && typeof network === 'object') { // Check bech32 prefix to determine network if ('bech32' in network) { switch (network.bech32) { case 'bc': return 'mainnet'; case 'tb': return 'testnet'; case 'bcrt': return 'regtest'; } } } // Default to mainnet if unable to determine return 'mainnet'; }