UNPKG

@btc-stamps/tx-builder

Version:

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

582 lines (513 loc) 14.5 kB
/** * ElectrumX Pooled Provider * High-level provider that uses connection pooling for better performance and reliability */ import type { Network } from 'bitcoinjs-lib'; import type { AddressHistory, AddressHistoryOptions, Balance, ProviderOptions, Transaction, UTXO, } from '../interfaces/provider.interface.ts'; import { getElectrumXEndpoints } from '../config/electrumx-config.ts'; import { BaseProvider } from './base-provider.ts'; import { type ConnectionPoolOptions, ElectrumXConnectionPool, type ElectrumXServer, } from './electrumx-connection-pool.ts'; export interface ElectrumXPooledOptions extends ProviderOptions { servers?: ElectrumXServer[]; poolOptions?: Partial<ConnectionPoolOptions>; } /** * ElectrumX Provider with built-in connection pooling and advanced features */ export class ElectrumXPooledProvider extends BaseProvider { private pool: ElectrumXConnectionPool; constructor(options: ElectrumXPooledOptions) { super(options); // Create connection pool this.pool = new ElectrumXConnectionPool({ network: this.network, servers: options.servers || this.getServersFromConfig(), connectionTimeout: this.timeout, retries: this.retries, retryDelay: this.retryDelay, maxRetryDelay: this.maxRetryDelay, ...options.poolOptions, }); } /** * Get UTXOs for a given address */ getUTXOs(address: string): Promise<UTXO[]> { if (!this.isValidAddress(address)) { throw new Error(`Invalid address: ${address}`); } return this.pool.getUTXOs(address); } /** * Get balance for a given address */ getBalance(address: string): Promise<Balance> { if (!this.isValidAddress(address)) { throw new Error(`Invalid address: ${address}`); } return this.pool.getBalance(address); } /** * Get transaction by ID */ getTransaction(txid: string): Promise<Transaction> { if (!this.isValidTxid(txid)) { throw new Error(`Invalid transaction ID: ${txid}`); } return this.pool.getTransaction(txid); } /** * Broadcast a signed transaction */ broadcastTransaction(hexTx: string): Promise<string> { if (!hexTx || typeof hexTx !== 'string' || hexTx.length < 20) { throw new Error('Invalid transaction hex'); } return this.pool.broadcastTransaction(hexTx); } /** * Get current fee rate (sat/vB) */ getFeeRate(priority: 'low' | 'medium' | 'high' = 'medium'): Promise<number> { return Promise.resolve(this.pool.getFeeRate(priority)); } /** * Get current block height */ getBlockHeight(): Promise<number> { return Promise.resolve(this.pool.getBlockHeight()); } /** * Get address transaction history */ getAddressHistory( address: string, options?: AddressHistoryOptions, ): Promise<AddressHistory[]> { if (!this.isValidAddress(address)) { throw new Error(`Invalid address: ${address}`); } return this.pool.getAddressHistory(address, options); } /** * Check if provider is connected (checks if any server is healthy) */ isConnected(): Promise<boolean> { const stats = this.pool.getStats(); return Promise.resolve(stats.servers.some((server) => server.healthy)); } /** * Get ElectrumX servers from centralized configuration */ private getServersFromConfig(): ElectrumXServer[] { // Get network name for configuration lookup const networkName = this.getNetworkName(); // Get endpoints from centralized config const endpoints = getElectrumXEndpoints(networkName); // Convert ElectrumXEndpoint to ElectrumXServer format return endpoints.map((endpoint, index) => ({ host: endpoint.host, port: endpoint.port, protocol: this.mapProtocol(endpoint.protocol), weight: this.calculateWeight(endpoint.priority || (index + 1)), region: this.guessRegion(endpoint.host), })); } /** * Get network name from Network object */ private getNetworkName(): string { if (this.network && typeof this.network === 'object') { // Check bech32 prefix to determine network if ('bech32' in this.network) { switch (this.network.bech32) { case 'bc': return 'mainnet'; case 'tb': return 'testnet'; case 'bcrt': return 'regtest'; } } } // Default to mainnet if unable to determine return 'mainnet'; } /** * Map ElectrumX protocol to connection pool protocol */ private mapProtocol( protocol: 'tcp' | 'ssl' | 'ws' | 'wss', ): 'tcp' | 'ssl' | 'ws' | 'wss' { // For now, they map directly, but this allows for future protocol handling return protocol; } /** * Calculate weight from priority (lower priority = higher weight) */ private calculateWeight(priority: number): number { // Convert priority (1, 2, 3...) to weight (3, 2, 1...) return Math.max(1, 4 - priority); } /** * Guess region from hostname (best effort) */ private guessRegion(host: string): string { const hostname = host.toLowerCase(); if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) { return 'local'; } // European indicators if ( hostname.includes('.de') || hostname.includes('.eu') || hostname.includes('europe') || hostname.includes('aranguren') || hostname.includes('emzy') ) { return 'eu'; } // US indicators if ( hostname.includes('.us') || hostname.includes('qtornado') || hostname.includes('fortress') ) { return 'us'; } // Asia indicators if ( hostname.includes('.jp') || hostname.includes('.cn') || hostname.includes('.sg') || hostname.includes('asia') ) { return 'asia'; } // Default to global for well-known global services return 'global'; } /** * Get connection pool statistics */ getPoolStats(): { 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; } { return this.pool.getStats(); } /** * Get detailed health information */ getHealthInfo(): { healthy: boolean; healthyServers: number; totalServers: number; totalConnections: number; averageSuccessRate: number; servers: Array<{ server: string; healthy: boolean; successRate: number; averageResponseTime: number; activeConnections: number; }>; } { const stats = this.pool.getStats(); const healthyServers = stats.servers.filter((s) => s.healthy); const averageSuccessRate = stats.servers.length > 0 ? stats.servers.reduce((sum, s) => sum + s.successRate, 0) / stats.servers.length : 0; return { healthy: healthyServers.length > 0, healthyServers: healthyServers.length, totalServers: stats.servers.length, totalConnections: stats.totalConnections, averageSuccessRate, servers: stats.servers.map((s) => ({ server: s.server, healthy: s.healthy, successRate: s.successRate, averageResponseTime: s.averageResponseTime, activeConnections: s.activeConnections, })), }; } /** * Test connection to all configured servers */ async testAllConnections(): Promise< Array<{ server: string; success: boolean; latency?: number; error?: string; }> > { const stats = this.pool.getStats(); const results = []; for (const server of stats.servers) { const startTime = Date.now(); try { // Test with a simple block height request await this.pool.getBlockHeight(); const latency = Date.now() - startTime; results.push({ server: server.server, success: true, latency, }); } catch (error) { results.push({ server: server.server, success: false, error: error instanceof Error ? error.message : String(error), }); } } return results; } /** * Get servers by region */ getServersByRegion(): Record<string, ElectrumXServer[]> { const serversList = this.getServersFromConfig(); const regions: Record<string, ElectrumXServer[]> = {}; for (const server of serversList) { const region = server.region || 'unknown'; if (!regions[region]) { regions[region] = []; } regions[region].push(server); } return regions; } /** * Update server configuration */ updateServers(servers: ElectrumXServer[]): void { // This would require extending the pool to support dynamic server updates console.warn( `Dynamic server updates not yet implemented for ${servers.length} servers. Restart provider to use new servers.`, ); } /** * Gracefully shutdown the provider */ async shutdown(): Promise<void> { await this.pool.shutdown(); } } /** * Create ElectrumX pooled provider with sensible defaults */ export function createElectrumXPooledProvider( network: Network, options?: Partial<ElectrumXPooledOptions>, ): ElectrumXPooledProvider { return new ElectrumXPooledProvider({ network, timeout: 30000, retries: 3, retryDelay: 1000, maxRetryDelay: 10000, ...options, }); } /** * Create ElectrumX pooled provider with custom server configuration */ export function createElectrumXPooledProviderWithServers( network: Network, servers: ElectrumXServer[], options?: Partial<ElectrumXPooledOptions>, ): ElectrumXPooledProvider { return new ElectrumXPooledProvider({ network, servers, timeout: 30000, retries: 3, retryDelay: 1000, maxRetryDelay: 10000, ...options, }); } /** * Create ElectrumX pooled provider optimized for specific use cases */ export class ElectrumXPooledProviderBuilder { private options: Partial<ElectrumXPooledOptions> = {}; constructor(private network: Network) {} /** * Set timeout values */ withTimeouts( timeout: number, retries: number = 3, retryDelay: number = 1000, ): this { this.options.timeout = timeout; this.options.retries = retries; this.options.retryDelay = retryDelay; return this; } /** * Configure for high-throughput applications */ forHighThroughput(): this { this.options.poolOptions = { ...this.options.poolOptions, maxConnectionsPerServer: 5, loadBalanceStrategy: 'least-connections', healthCheckInterval: 15000, // 15 seconds }; return this; } /** * Configure for low-latency applications */ forLowLatency(): this { this.options.poolOptions = { ...this.options.poolOptions, maxConnectionsPerServer: 2, loadBalanceStrategy: 'health-based', healthCheckInterval: 10000, // 10 seconds connectionTimeout: 5000, }; return this; } /** * Configure for high reliability */ forHighReliability(): this { this.options.poolOptions = { ...this.options.poolOptions, failoverThreshold: 2, // Lower threshold recoveryTimeout: 30000, // Faster recovery healthCheckInterval: 10000, }; this.options.retries = 5; return this; } /** * Add custom servers */ withServers(servers: ElectrumXServer[]): this { this.options.servers = servers; return this; } /** * Filter servers by region from configured endpoints */ withRegion(region: 'us' | 'eu' | 'asia' | 'global' | 'local'): this { // Get all configured servers const networkName = this.getNetworkName(); const endpoints = getElectrumXEndpoints(networkName); // Convert to servers and filter by region const allServers = endpoints.map((endpoint, index) => ({ host: endpoint.host, port: endpoint.port, protocol: endpoint.protocol as 'tcp' | 'ssl' | 'ws' | 'wss', weight: Math.max(1, 4 - (endpoint.priority || (index + 1))), region: this.guessRegionForBuilder(endpoint.host), })); // Filter by requested region const regionalServers = allServers.filter((server) => server.region === region); if (regionalServers.length === 0) { console.warn( `No servers found for region '${region}'. Using all available servers.`, ); this.options.servers = allServers; } else { this.options.servers = regionalServers; } return this; } /** * Guess region from hostname for builder (helper method) */ private guessRegionForBuilder(host: string): string { const hostname = host.toLowerCase(); if (hostname.includes('localhost') || hostname.includes('127.0.0.1')) { return 'local'; } // European indicators if ( hostname.includes('.de') || hostname.includes('.eu') || hostname.includes('europe') || hostname.includes('aranguren') || hostname.includes('emzy') ) { return 'eu'; } // US indicators if ( hostname.includes('.us') || hostname.includes('qtornado') || hostname.includes('fortress') ) { return 'us'; } // Asia indicators if ( hostname.includes('.jp') || hostname.includes('.cn') || hostname.includes('.sg') || hostname.includes('asia') ) { return 'asia'; } return 'global'; } /** * Get network name for builder */ private getNetworkName(): string { if (this.network && typeof this.network === 'object') { if ('bech32' in this.network) { switch (this.network.bech32) { case 'bc': return 'mainnet'; case 'tb': return 'testnet'; case 'bcrt': return 'regtest'; } } } return 'mainnet'; } /** * Build the provider */ build(): ElectrumXPooledProvider { return new ElectrumXPooledProvider({ network: this.network, timeout: 30000, retries: 3, retryDelay: 1000, maxRetryDelay: 10000, ...this.options, }); } }