UNPKG

@btc-stamps/tx-builder

Version:

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

651 lines (564 loc) 17.5 kB
/** * Streaming UTXO Processor * Handles large UTXO sets efficiently using streaming and chunking */ import type { SelectionOptions, SelectionResult, UTXO } from '../interfaces/selector.interface.ts'; import { isSelectionSuccess } from '../interfaces/selector-result.interface.ts'; import { PerformanceMonitor } from './performance-monitor.ts'; import { UTXOCacheManager } from './utxo-cache-manager.ts'; export interface StreamingConfig { chunkSize: number; // Number of UTXOs to process at once maxMemoryMB: number; // Memory limit for processing enableCompression: boolean; // Compress UTXO data sortStrategy: 'value' | 'confirmations' | 'age' | 'none'; filterStrategy: 'aggressive' | 'moderate' | 'conservative'; enablePrefetch: boolean; // Prefetch next chunks concurrentChunks: number; // Number of chunks to process concurrently } export interface UTXOChunk { id: string; utxos: UTXO[]; totalValue: number; averageValue: number; count: number; memorySize: number; processed: boolean; } export interface StreamingStats { totalUTXOs: number; chunksProcessed: number; memoryUsage: number; filterEfficiency: number; // Percentage of UTXOs filtered out processingSpeed: number; // UTXOs per second cacheHitRate: number; } /** * Streaming UTXO processor for handling large datasets */ export class StreamingUTXOProcessor { private config: Required<StreamingConfig>; private _performanceMonitor: PerformanceMonitor; private cacheManager: UTXOCacheManager; private processedChunks = new Map<string, UTXOChunk>(); private memoryUsage = 0; private stats: StreamingStats = { totalUTXOs: 0, chunksProcessed: 0, memoryUsage: 0, filterEfficiency: 0, processingSpeed: 0, cacheHitRate: 0, }; constructor( performanceMonitor: PerformanceMonitor, cacheManager: UTXOCacheManager, config?: Partial<StreamingConfig>, ) { this._performanceMonitor = performanceMonitor; this.cacheManager = cacheManager; // Use performance monitor for future performance tracking void this._performanceMonitor; this.config = { chunkSize: 1000, maxMemoryMB: 100, enableCompression: true, sortStrategy: 'value', filterStrategy: 'moderate', enablePrefetch: true, concurrentChunks: 3, ...config, }; } /** * Process large UTXO set using streaming approach */ async processLargeUTXOSet( utxoSource: AsyncIterable<UTXO> | UTXO[], options: SelectionOptions, onProgress?: ( progress: { processed: number; total?: number; found?: boolean }, ) => void, ): Promise<SelectionResult | null> { const startTime = Date.now(); this.resetStats(); try { // Convert array to async iterable if needed const utxoStream = Array.isArray(utxoSource) ? this.arrayToAsyncIterable(utxoSource) : utxoSource; // Process UTXOs in chunks let bestResult: SelectionResult | null = null; let totalProcessed = 0; let found = false; for await (const chunk of this.chunkStream(utxoStream)) { const chunkResult = this.processChunk(chunk, options); totalProcessed += chunk.count; if (chunkResult) { // Check if this is better than previous results if ( !bestResult || this.isResultBetter(chunkResult, bestResult, options) ) { bestResult = chunkResult; found = true; } } // Update progress onProgress?.({ processed: totalProcessed, total: this.stats.totalUTXOs, found, }); // Early exit if we found a good enough result if (bestResult && this.isResultGoodEnough(bestResult, options)) { break; } // Memory management await this.manageMemory(); } // Update statistics const processingTime = Date.now() - startTime; this.stats.processingSpeed = totalProcessed / (processingTime / 1000); return bestResult; } catch (error) { console.error('Streaming UTXO processing failed:', error); return null; } } /** * Stream UTXOs by value range for efficient filtering */ async *streamUTXOsByValueRange( utxoSource: AsyncIterable<UTXO> | UTXO[], minValue: number, maxValue: number, ): AsyncGenerator<UTXO> { const utxoStream = Array.isArray(utxoSource) ? this.arrayToAsyncIterable(utxoSource) : utxoSource; for await (const utxo of utxoStream) { if (utxo.value >= minValue && utxo.value <= maxValue) { yield utxo; } } } /** * Prefetch and cache UTXOs based on selection patterns */ async prefetchOptimalUTXOs( address: string, options: SelectionOptions, provider: any, // IProvider interface ): Promise<UTXO[]> { try { // Check cache first const cachedUTXOs = this.cacheManager.getOptimizedUTXOs(address, options); if (cachedUTXOs) { this.stats.cacheHitRate += 1; return cachedUTXOs; } // Fetch UTXOs from provider const allUTXOs = await provider.getUTXOs(address); // Apply intelligent filtering const optimizedUTXOs = this.prefilterUTXOs(allUTXOs, options); // Cache the optimized set this.cacheManager.setUTXOs(address, optimizedUTXOs); return optimizedUTXOs; } catch (error) { console.error('UTXO prefetch failed:', error); return []; } } /** * Create indexed UTXO structure for faster searching */ createUTXOIndex(utxos: UTXO[]): { byValue: Map<number, UTXO[]>; byConfirmations: Map<number, UTXO[]>; sortedByValue: UTXO[]; valueRanges: Array<{ min: number; max: number; utxos: UTXO[] }>; } { const byValue = new Map<number, UTXO[]>(); const byConfirmations = new Map<number, UTXO[]>(); // Create value buckets (logarithmic) for (const utxo of utxos) { const valueBucket = Math.floor(Math.log10(utxo.value)); if (!byValue.has(valueBucket)) { byValue.set(valueBucket, []); } byValue.get(valueBucket)!.push(utxo); // Create confirmation buckets const confirmationBucket = Math.min(utxo.confirmations ?? 0, 100); if (!byConfirmations.has(confirmationBucket)) { byConfirmations.set(confirmationBucket, []); } byConfirmations.get(confirmationBucket)!.push(utxo); } // Sort by value const sortedByValue = [...utxos].sort((a, b) => a.value - b.value); // Create value ranges for efficient range queries const valueRanges = this.createValueRanges(sortedByValue); return { byValue, byConfirmations, sortedByValue, valueRanges, }; } /** * Get statistics about streaming processing */ getStats(): StreamingStats { return { ...this.stats }; } /** * Update configuration */ updateConfig(newConfig: Partial<StreamingConfig>): void { this.config = { ...this.config, ...newConfig }; } /** * Clear processed chunks and reset memory */ cleanup(): void { this.processedChunks.clear(); this.memoryUsage = 0; this.resetStats(); } /** * Convert array to async iterable */ private async *arrayToAsyncIterable(array: UTXO[]): AsyncGenerator<UTXO> { this.stats.totalUTXOs = array.length; for (const item of array) { yield item; } } /** * Create chunks from UTXO stream */ private async *chunkStream( utxoStream: AsyncIterable<UTXO>, ): AsyncGenerator<UTXOChunk> { let chunk: UTXO[] = []; let chunkId = 0; for await (const utxo of utxoStream) { chunk.push(utxo); if (chunk.length >= this.config.chunkSize) { yield this.createChunk(`chunk-${chunkId++}`, chunk); chunk = []; } } // Yield remaining UTXOs if (chunk.length > 0) { yield this.createChunk(`chunk-${chunkId}`, chunk); } } /** * Create chunk object */ private createChunk(id: string, utxos: UTXO[]): UTXOChunk { const totalValue = utxos.reduce((sum, utxo) => sum + utxo.value, 0); const averageValue = totalValue / utxos.length; const memorySize = this.estimateMemorySize(utxos); // Apply sorting if configured const sortedUTXOs = this.sortUTXOs(utxos); const chunk: UTXOChunk = { id, utxos: sortedUTXOs, totalValue, averageValue, count: utxos.length, memorySize, processed: false, }; this.processedChunks.set(id, chunk); this.memoryUsage += memorySize; return chunk; } /** * Process individual chunk */ private processChunk( chunk: UTXOChunk, options: SelectionOptions, ): SelectionResult | null { // Pre-filter chunk UTXOs const filteredUTXOs = this.prefilterUTXOs(chunk.utxos, options); if (filteredUTXOs.length === 0) { chunk.processed = true; return null; } // Try selection with filtered UTXOs // For now, we'll use a simple accumulative approach // In a real implementation, this would delegate to the selector factory const result = this.attemptSelection(filteredUTXOs, options); chunk.processed = true; this.stats.chunksProcessed++; // Update filter efficiency const filtered = chunk.count - filteredUTXOs.length; this.stats.filterEfficiency = (this.stats.filterEfficiency + filtered / chunk.count) / 2; return result; } /** * Simple accumulative selection for chunk processing */ private attemptSelection( utxos: UTXO[], options: SelectionOptions, ): SelectionResult | null { const selected: UTXO[] = []; let accumulated = 0; const targetWithFee = options.targetValue + this.estimateFee(3, 2, options.feeRate); for (const utxo of utxos) { selected.push(utxo); accumulated += utxo.value; if (accumulated >= targetWithFee) { const fee = this.estimateFee(selected.length, 2, options.feeRate); const change = accumulated - options.targetValue - fee; if (accumulated >= options.targetValue + fee) { const estimatedVSize = this.estimateTransactionSize(selected.length, 2); return { success: true, inputs: selected, fee, change: Math.max(0, change), totalValue: accumulated, inputCount: selected.length, outputCount: 2, // Target + change estimatedVSize, effectiveFeeRate: fee / estimatedVSize, }; } } } return null; } /** * Estimate transaction fee */ private estimateFee( inputCount: number, outputCount: number, feeRate: number, ): number { // Simplified fee calculation const vSize = this.estimateTransactionSize(inputCount, outputCount); return vSize * feeRate; } /** * Estimate transaction size in vBytes */ private estimateTransactionSize( inputCount: number, outputCount: number, ): number { const inputSize = inputCount * 148; // Average input size const outputSize = outputCount * 34; // Average output size const overhead = 10; // Transaction overhead return inputSize + outputSize + overhead; } /** * Pre-filter UTXOs based on selection options */ private prefilterUTXOs(utxos: UTXO[], options: SelectionOptions): UTXO[] { let filtered = [...utxos]; // Filter by minimum confirmations if (options.minConfirmations) { filtered = filtered.filter((utxo) => (utxo.confirmations ?? 0) >= options.minConfirmations!); } // Filter out dust UTXOs const dustThreshold = options.dustThreshold || 546; filtered = filtered.filter((utxo) => utxo.value > dustThreshold); // Apply filtering strategy switch (this.config.filterStrategy) { case 'aggressive': { // Remove very small UTXOs relative to target const minRelevantValue = options.targetValue * 0.001; filtered = filtered.filter((utxo) => utxo.value >= minRelevantValue); break; } case 'moderate': { // Remove UTXOs that are clearly too small const moderateMinValue = Math.max( dustThreshold * 2, options.targetValue * 0.0001, ); filtered = filtered.filter((utxo) => utxo.value >= moderateMinValue); break; } case 'conservative': // Only remove obvious dust break; } return filtered; } /** * Sort UTXOs based on configured strategy */ private sortUTXOs(utxos: UTXO[]): UTXO[] { switch (this.config.sortStrategy) { case 'value': { return [...utxos].sort((a, b) => b.value - a.value); } case 'confirmations': { return [...utxos].sort((a, b) => (b.confirmations ?? 0) - (a.confirmations ?? 0)); } case 'age': { // Sort by confirmations as proxy for age return [...utxos].sort((a, b) => (b.confirmations ?? 0) - (a.confirmations ?? 0)); } case 'none': default: { return utxos; } } } /** * Check if result is better than current best */ private isResultBetter( newResult: SelectionResult, currentBest: SelectionResult, options: SelectionOptions, ): boolean { // Only compare successful results if (!isSelectionSuccess(newResult) || !isSelectionSuccess(currentBest)) { return false; } // Prefer results with fewer inputs if (newResult.inputs.length < currentBest.inputs.length) { return true; } // Prefer lower fees if ( newResult.fee < currentBest.fee && newResult.inputs.length <= currentBest.inputs.length ) { return true; } // Prefer changeless transactions const dustThreshold = options.dustThreshold || 546; const newChangeless = newResult.change < dustThreshold; const currentChangeless = currentBest.change < dustThreshold; if (newChangeless && !currentChangeless) { return true; } return false; } /** * Check if result is good enough to stop searching */ private isResultGoodEnough( result: SelectionResult, options: SelectionOptions, ): boolean { if (!isSelectionSuccess(result)) { return false; } const dustThreshold = options.dustThreshold || 546; const isChangeless = result.change < dustThreshold; const hasMinimalInputs = result.inputs.length <= 3; return isChangeless && hasMinimalInputs; } /** * Estimate memory size of UTXO array */ private estimateMemorySize(utxos: UTXO[]): number { // Rough estimate: each UTXO is about 200 bytes return utxos.length * 200; } /** * Manage memory usage by cleaning up old chunks */ private manageMemory(): void { const maxMemoryBytes = this.config.maxMemoryMB * 1024 * 1024; if (this.memoryUsage > maxMemoryBytes) { const chunksToRemove = Array.from(this.processedChunks.entries()) .filter(([_, chunk]) => chunk.processed) .sort(([a], [b]) => a.localeCompare(b)) // Remove oldest chunks first .slice(0, Math.floor(this.processedChunks.size * 0.3)); // Remove 30% for (const [id, chunk] of chunksToRemove) { this.processedChunks.delete(id); this.memoryUsage -= chunk.memorySize; } } } /** * Create value ranges for efficient range queries */ private createValueRanges( sortedUTXOs: UTXO[], ): Array<{ min: number; max: number; utxos: UTXO[] }> { const ranges = []; const rangeSize = Math.ceil(sortedUTXOs.length / 10); // 10 ranges for (let i = 0; i < sortedUTXOs.length; i += rangeSize) { const rangeUTXOs = sortedUTXOs.slice(i, i + rangeSize); if (rangeUTXOs.length > 0) { ranges.push({ min: rangeUTXOs[0]!.value, max: rangeUTXOs[rangeUTXOs.length - 1]!.value, utxos: rangeUTXOs, }); } } return ranges; } /** * Reset statistics */ private resetStats(): void { this.stats = { totalUTXOs: 0, chunksProcessed: 0, memoryUsage: 0, filterEfficiency: 0, processingSpeed: 0, cacheHitRate: 0, }; } } /** * Create streaming UTXO processor */ export function createStreamingUTXOProcessor( performanceMonitor: PerformanceMonitor, cacheManager: UTXOCacheManager, config?: Partial<StreamingConfig>, ): StreamingUTXOProcessor { return new StreamingUTXOProcessor(performanceMonitor, cacheManager, config); } /** * Create memory-optimized streaming processor */ export function createMemoryOptimizedStreamingProcessor( performanceMonitor: PerformanceMonitor, cacheManager: UTXOCacheManager, ): StreamingUTXOProcessor { return new StreamingUTXOProcessor(performanceMonitor, cacheManager, { chunkSize: 500, maxMemoryMB: 50, enableCompression: true, sortStrategy: 'value', filterStrategy: 'aggressive', enablePrefetch: false, concurrentChunks: 2, }); } /** * Create high-performance streaming processor */ export function createHighPerformanceStreamingProcessor( performanceMonitor: PerformanceMonitor, cacheManager: UTXOCacheManager, ): StreamingUTXOProcessor { return new StreamingUTXOProcessor(performanceMonitor, cacheManager, { chunkSize: 2000, maxMemoryMB: 200, enableCompression: false, sortStrategy: 'value', filterStrategy: 'moderate', enablePrefetch: true, concurrentChunks: 4, }); }