UNPKG

@btc-stamps/tx-builder

Version:

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

524 lines (465 loc) 14.7 kB
/** * ElectrumX Configuration for tx-builder * Supports centralized configuration with environment variable support */ import type { Network } from 'bitcoinjs-lib'; import process from 'node:process'; export interface ElectrumXEndpoint { host: string; port: number; protocol: 'tcp' | 'ssl' | 'ws' | 'wss'; maxRetries?: number; timeout?: number; priority?: number; // Lower number = higher priority description?: string; // Human-readable description } export interface ElectrumXConfig { endpoints: ElectrumXEndpoint[]; network: Network; fallbackToPublic?: boolean; connectionTimeout?: number; requestTimeout?: number; maxRetries?: number; } /** * Default public mainnet ElectrumX/Electrs servers * These are high-performance, reliable servers validated through automated testing * Used when no custom servers are configured via environment variables * Sorted by performance (response time) for optimal connection reliability */ export const DEFAULT_MAINNET_SERVERS: ElectrumXEndpoint[] = [ { host: 'blockstream.info', port: 50002, protocol: 'ssl', priority: 1, timeout: 10000, maxRetries: 3, description: 'Blockstream SSL (50002 - standard ElectrumX SSL port)', }, { host: 'blockstream.info', port: 50001, protocol: 'tcp', priority: 2, timeout: 10000, maxRetries: 3, description: 'Blockstream TCP (50001 - standard ElectrumX TCP port)', }, { host: 'fortress.qtornado.com', port: 443, protocol: 'ssl', priority: 3, timeout: 10000, maxRetries: 3, description: 'QTornado SSL (180ms avg) - Fast US server', }, { host: 'electrum1.bluewallet.io', port: 443, protocol: 'ssl', priority: 4, timeout: 10000, maxRetries: 3, description: 'BlueWallet SSL (194ms avg) - Mobile optimized', }, { host: 'bitcoin.aranguren.org', port: 50001, protocol: 'tcp', priority: 5, timeout: 10000, maxRetries: 3, description: 'BitKoyn TCP (stable)', }, { host: 'btc.smsys.me', port: 50002, protocol: 'ssl', priority: 6, timeout: 10000, maxRetries: 3, description: '1209k SSL (backup)', }, ]; /** * Legacy fallback name for backward compatibility * @deprecated Use DEFAULT_MAINNET_SERVERS instead */ export const FALLBACK_MAINNET_ENDPOINTS = DEFAULT_MAINNET_SERVERS; /** * Default public testnet ElectrumX/Electrs servers * These are high-performance, reliable servers validated through automated testing * Used when no custom servers are configured via environment variables * Sorted by performance (response time) for optimal connection reliability */ export const DEFAULT_TESTNET_SERVERS: ElectrumXEndpoint[] = [ { host: 'blockstream.info', port: 993, protocol: 'ssl', priority: 1, timeout: 10000, maxRetries: 3, description: 'Blockstream Testnet SSL (105ms avg) - Most reliable', }, { host: 'blockstream.info', port: 143, protocol: 'tcp', priority: 2, timeout: 10000, maxRetries: 3, description: 'Blockstream Testnet TCP (107ms avg) - Primary TCP', }, { host: 'testnet.qtornado.com', port: 51002, protocol: 'ssl', priority: 3, timeout: 10000, maxRetries: 3, description: 'QTornado Testnet SSL (192ms avg) - US server', }, { host: 'testnet.aranguren.org', port: 51002, protocol: 'ssl', priority: 4, timeout: 10000, maxRetries: 3, description: 'SpeedWallet Testnet SSL (stable)', }, { host: 'testnet.aranguren.org', port: 51001, protocol: 'tcp', priority: 5, timeout: 10000, maxRetries: 3, description: 'SpeedWallet Testnet TCP (backup)', }, ]; /** * Legacy fallback name for backward compatibility * @deprecated Use DEFAULT_TESTNET_SERVERS instead */ export const FALLBACK_TESTNET_ENDPOINTS = DEFAULT_TESTNET_SERVERS; /** * Default local regtest endpoints * Used when no custom servers are configured via environment variables * For local development and testing environments */ export const DEFAULT_REGTEST_SERVERS: ElectrumXEndpoint[] = [ { host: 'localhost', port: 50001, protocol: 'tcp', priority: 1, timeout: 5000, maxRetries: 2, description: 'Local regtest server (localhost)', }, { host: '127.0.0.1', port: 50001, protocol: 'tcp', priority: 2, timeout: 5000, maxRetries: 2, description: 'Local regtest server (IP)', }, ]; /** * Legacy fallback name for backward compatibility * @deprecated Use DEFAULT_REGTEST_SERVERS instead */ export const FALLBACK_REGTEST_ENDPOINTS = DEFAULT_REGTEST_SERVERS; /** * Parse ElectrumX servers from environment variable string * Format: "host1:port1:protocol1,host2:port2:protocol2" * Example: "electrum.blockstream.info:50002:ssl,fortress.qtornado.com:443:ssl" */ export function parseServersFromEnv(envString: string): ElectrumXEndpoint[] { const endpoints: ElectrumXEndpoint[] = []; if (!envString || envString.trim() === '') { return endpoints; } const serverStrings = envString.split(','); serverStrings.forEach((serverString, index) => { const parts = serverString.trim().split(':'); if (parts.length >= 2) { const host = parts[0]?.trim(); const port = parseInt(parts[1]?.trim() || '0'); const protocol = parts[2]?.trim() as 'tcp' | 'ssl' | 'ws' | 'wss' || 'ssl'; if (host && !isNaN(port) && port > 0 && port <= 65535) { endpoints.push({ host, port, protocol, priority: index + 1, description: `Environment server ${index + 1}`, }); } else { console.warn(`Invalid ElectrumX server format: ${serverString}`); } } }); return endpoints; } /** * Get ElectrumX endpoints with smart configuration loading * Priority: Network-specific env vars > Generic env vars > High-performance defaults * Logs which configuration source is being used for debugging */ export function getElectrumXEndpoints( networkName: string, ): ElectrumXEndpoint[] { const network = networkName.toLowerCase(); // Check for network-specific environment variables first let envServers: ElectrumXEndpoint[] = []; let configSource = ''; switch (network) { case 'mainnet': case 'bitcoin': if (process.env.ELECTRUMX_MAINNET_SERVERS) { envServers = parseServersFromEnv(process.env.ELECTRUMX_MAINNET_SERVERS); configSource = 'ELECTRUMX_MAINNET_SERVERS environment variable'; } break; case 'testnet': case 'testnet3': if (process.env.ELECTRUMX_TESTNET_SERVERS) { envServers = parseServersFromEnv(process.env.ELECTRUMX_TESTNET_SERVERS); configSource = 'ELECTRUMX_TESTNET_SERVERS environment variable'; } break; case 'regtest': case 'regtest1': if (process.env.ELECTRUMX_REGTEST_SERVERS) { envServers = parseServersFromEnv(process.env.ELECTRUMX_REGTEST_SERVERS); configSource = 'ELECTRUMX_REGTEST_SERVERS environment variable'; } break; } // Fall back to generic ELECTRUMX_SERVERS if no network-specific servers if (envServers.length === 0 && process.env.ELECTRUMX_SERVERS) { envServers = parseServersFromEnv(process.env.ELECTRUMX_SERVERS); configSource = 'ELECTRUMX_SERVERS environment variable'; } // Return environment servers if found if (envServers.length > 0) { console.log( `ElectrumX: Using ${envServers.length} server(s) from ${configSource}`, ); return envServers; } // Return high-performance defaults based on network const defaults = getDefaultEndpoints(network); console.log( `ElectrumX: Using ${defaults.length} default ${network} server(s) (no environment config found)`, ); return defaults; } /** * Get default high-performance endpoints for a given network * Returns validated, fast public servers when no custom configuration is provided */ export function getDefaultEndpoints(networkName: string): ElectrumXEndpoint[] { switch (networkName.toLowerCase()) { case 'testnet': case 'testnet3': return [...DEFAULT_TESTNET_SERVERS]; case 'regtest': case 'regtest1': return [...DEFAULT_REGTEST_SERVERS]; case 'mainnet': case 'bitcoin': default: return [...DEFAULT_MAINNET_SERVERS]; } } /** * Legacy fallback function for backward compatibility * @deprecated Use getDefaultEndpoints instead */ export function getFallbackEndpoints(networkName: string): ElectrumXEndpoint[] { console.warn( 'getFallbackEndpoints is deprecated, use getDefaultEndpoints instead', ); return getDefaultEndpoints(networkName); } /** * Validate endpoint configuration */ export function validateEndpoint(endpoint: ElectrumXEndpoint): string[] { const errors: string[] = []; if (!endpoint.host) { errors.push('Endpoint host is required'); } if (!endpoint.port || endpoint.port < 1 || endpoint.port > 65535) { errors.push('Endpoint port must be between 1 and 65535'); } if (!['tcp', 'ssl', 'ws', 'wss'].includes(endpoint.protocol)) { errors.push('Endpoint protocol must be one of: tcp, ssl, ws, wss'); } if (endpoint.timeout && endpoint.timeout < 1000) { errors.push('Endpoint timeout must be at least 1000ms'); } if (endpoint.maxRetries && endpoint.maxRetries < 0) { errors.push('Endpoint maxRetries cannot be negative'); } return errors; } /** * Validate configuration */ export function validateConfig(config: ElectrumXConfig): string[] { const errors: string[] = []; if (!config.endpoints || config.endpoints.length === 0) { errors.push('At least one endpoint is required'); } else { config.endpoints.forEach((endpoint, index) => { const endpointErrors = validateEndpoint(endpoint); endpointErrors.forEach((error) => { errors.push(`Endpoint ${index}: ${error}`); }); }); } if (!config.network) { errors.push('Network is required'); } if (config.connectionTimeout && config.connectionTimeout < 1000) { errors.push('Connection timeout must be at least 1000ms'); } if (config.requestTimeout && config.requestTimeout < 1000) { errors.push('Request timeout must be at least 1000ms'); } return errors; } /** * Load ElectrumX configuration from environment variables */ export function loadElectrumXConfigFromEnv(): Partial<ElectrumXConfig> { const config: Partial<ElectrumXConfig> = {}; // Connection timeout if (process.env.ELECTRUMX_TIMEOUT) { const timeout = parseInt(process.env.ELECTRUMX_TIMEOUT); if (!isNaN(timeout) && timeout > 0) { config.connectionTimeout = timeout; config.requestTimeout = timeout; } } // Max retries if (process.env.ELECTRUMX_MAX_RETRIES) { const retries = parseInt(process.env.ELECTRUMX_MAX_RETRIES); if (!isNaN(retries) && retries >= 0) { config.maxRetries = retries; } } // Fallback to public servers if (process.env.ELECTRUMX_FALLBACK_TO_PUBLIC !== undefined) { config.fallbackToPublic = process.env.ELECTRUMX_FALLBACK_TO_PUBLIC.toLowerCase() === 'true'; } return config; } /** * Create a complete ElectrumX configuration for a network */ export function createElectrumXConfig(network: Network): ElectrumXConfig { const networkName = getNetworkName(network); const envConfig = loadElectrumXConfigFromEnv(); const endpoints = getElectrumXEndpoints(networkName); const config: ElectrumXConfig = { endpoints, network, fallbackToPublic: true, connectionTimeout: 10000, // 10 seconds requestTimeout: 30000, // 30 seconds maxRetries: 3, ...envConfig, }; // Validate the configuration const errors = validateConfig(config); if (errors.length > 0) { throw new Error(`Invalid ElectrumX configuration: ${errors.join(', ')}`); } return config; } /** * Get network name from Network object or string */ function getNetworkName(network: Network | string): string { if (typeof network === 'string') { return network; } // Handle bitcoinjs-lib Network objects 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'; } } // Check message prefix as fallback if ( 'messagePrefix' in network && typeof network.messagePrefix === 'string' ) { if (network.messagePrefix.includes('Bitcoin Signed Message')) { return 'mainnet'; } if (network.messagePrefix.includes('testnet')) { return 'testnet'; } if (network.messagePrefix.includes('regtest')) { return 'regtest'; } } } // Default to mainnet if unable to determine return 'mainnet'; } /** * Get configuration documentation with environment variable examples */ export function getElectrumXConfigDocumentation(): string { return ` ElectrumX Server Configuration Environment Variables (recommended): Network-specific servers (highest priority): ELECTRUMX_MAINNET_SERVERS="server1.example.com:50002:ssl,server2.example.com:50001:tcp" ELECTRUMX_TESTNET_SERVERS="testnet1.example.com:50002:ssl,testnet2.example.com:50001:tcp" ELECTRUMX_REGTEST_SERVERS="localhost:50001:tcp,127.0.0.1:50001:tcp" Generic servers (medium priority): ELECTRUMX_SERVERS="server1.example.com:50002:ssl,server2.example.com:50001:tcp" Connection settings: ELECTRUMX_TIMEOUT=10000 # Connection and request timeout in ms (default: 10000) ELECTRUMX_MAX_RETRIES=3 # Maximum retry attempts (default: 3) ELECTRUMX_FALLBACK_TO_PUBLIC=true # Use fallback servers if custom fail (default: true) Server format: "host:port:protocol" Supported protocols: tcp, ssl, ws, wss Examples: # Production mainnet setup ELECTRUMX_MAINNET_SERVERS="electrum.mycompany.com:50002:ssl,backup.mycompany.com:50002:ssl" ELECTRUMX_TIMEOUT=15000 ELECTRUMX_MAX_RETRIES=5 # Development with local regtest ELECTRUMX_REGTEST_SERVERS="localhost:50001:tcp" ELECTRUMX_FALLBACK_TO_PUBLIC=false # Multi-network with specific servers ELECTRUMX_MAINNET_SERVERS="mainnet.example.com:50002:ssl" ELECTRUMX_TESTNET_SERVERS="testnet.example.com:50002:ssl" ELECTRUMX_REGTEST_SERVERS="localhost:50001:tcp" Fallback behavior: - If no environment variables are set, uses reliable public servers - Network-specific env vars override generic ELECTRUMX_SERVERS - Custom servers are always tried before fallback servers `; }