UNPKG

@btc-stamps/tx-builder

Version:

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

234 lines (201 loc) 6.19 kB
/** * TCP/SSL Client for ElectrumX connections * Provides direct TCP socket support for ElectrumX servers */ import * as net from 'node:net'; import * as tls from 'node:tls'; import { EventEmitter } from 'node:events'; import { Buffer } from 'node:buffer'; import type { ElectrumXEndpoint } from '../config/electrumx-config.ts'; import { clearTimeoutCompat, setTimeoutCompat, type TimerId } from '../utils/timer-utils.ts'; export interface TCPClientOptions { timeout?: number; keepAlive?: boolean; rejectUnauthorized?: boolean; } export class ElectrumXTCPClient extends EventEmitter { private socket: net.Socket | tls.TLSSocket | null = null; private buffer = ''; private requestId = 1; private pendingRequests = new Map<number, { resolve: (value: any) => void; reject: (error: any) => void; method: string; timer?: TimerId; }>(); private connected = false; private endpoint: ElectrumXEndpoint | null = null; constructor(private options: TCPClientOptions = {}) { super(); } /** * Connect to ElectrumX server via TCP or SSL */ connect(endpoint: ElectrumXEndpoint): Promise<void> { if (this.connected) { throw new Error('Already connected'); } this.endpoint = endpoint; return new Promise((resolve, reject) => { const timeout = setTimeoutCompat(() => { reject( new Error(`Connection timeout to ${endpoint.host}:${endpoint.port}`), ); this.cleanup(); }, this.options.timeout || endpoint.timeout || 10000); const onConnect = () => { clearTimeoutCompat(timeout); this.connected = true; this.emit('connected'); resolve(); }; const onData = (data: Buffer) => { this.buffer += data.toString(); const lines = this.buffer.split('\n'); this.buffer = lines.pop() || ''; for (const line of lines) { if (line.trim()) { try { const response = JSON.parse(line); this.handleResponse(response); } catch (e) { console.error('Failed to parse response:', e); console.error('Raw line:', line); } } } }; const onError = (err: Error) => { clearTimeoutCompat(timeout); // Enhanced error handling for ECONNRESET and other connection issues if (err.message.includes('ECONNRESET')) { console.warn( `ECONNRESET detected for ${endpoint.host}:${endpoint.port} - connection was reset by peer`, ); } else if (err.message.includes('ECONNREFUSED')) { console.warn( `ECONNREFUSED detected for ${endpoint.host}:${endpoint.port} - connection refused`, ); } else if (err.message.includes('EHOSTUNREACH')) { console.warn( `EHOSTUNREACH detected for ${endpoint.host}:${endpoint.port} - host unreachable`, ); } this.emit('error', err); if (!this.connected) { reject(err); } this.cleanup(); }; const onClose = () => { this.emit('disconnected'); this.cleanup(); }; // Create appropriate socket based on protocol if (endpoint.protocol === 'ssl') { this.socket = tls.connect({ host: endpoint.host, port: endpoint.port, rejectUnauthorized: this.options.rejectUnauthorized !== undefined ? this.options.rejectUnauthorized : false, // Default to false for ElectrumX servers which often use self-signed certs }); } else if (endpoint.protocol === 'tcp') { // Use plain TCP connection this.socket = net.createConnection({ host: endpoint.host, port: endpoint.port, }); } else { reject(new Error(`Unsupported protocol: ${endpoint.protocol}`)); return; } // Set up event handlers this.socket.on('connect', onConnect); this.socket.on('data', onData); this.socket.on('error', onError); this.socket.on('close', onClose); // Keep alive if (this.options.keepAlive) { this.socket.setKeepAlive(true, 60000); } }); } /** * Send request to ElectrumX server */ request(method: string, params: any[] = []): Promise<any> { if (!this.connected || !this.socket) { throw new Error('Not connected'); } const id = this.requestId++; return new Promise((resolve, reject) => { // Set up response handler const timer = setTimeoutCompat(() => { this.pendingRequests.delete(id); reject(new Error(`Request timeout: ${method}`)); }, 30000); this.pendingRequests.set(id, { resolve, reject, method, timer, }); // Send request const request = JSON.stringify({ id, method, params }) + '\n'; this.socket!.write(request); }); } /** * Handle response from server */ private handleResponse(response: any): void { const handler = this.pendingRequests.get(response.id); if (handler) { clearTimeoutCompat(handler.timer); this.pendingRequests.delete(response.id); if (response.error) { handler.reject(new Error(response.error.message || 'Server error')); } else { handler.resolve(response.result); } } } /** * Disconnect from server */ disconnect(): void { this.cleanup(); } /** * Clean up resources */ private cleanup(): void { this.connected = false; // Clear pending requests for (const [_id, handler] of this.pendingRequests) { clearTimeoutCompat(handler.timer); handler.reject(new Error('Connection closed')); } this.pendingRequests.clear(); // Close socket if (this.socket) { this.socket.destroy(); this.socket = null; } this.buffer = ''; this.endpoint = null; } /** * Check if connected */ isConnected(): boolean { return this.connected; } /** * Get current endpoint */ getEndpoint(): ElectrumXEndpoint | null { return this.endpoint; } }