UNPKG

@btc-stamps/tx-builder

Version:

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

518 lines (457 loc) 15 kB
/** * UTXO Selector Factory * Creates selector instances based on algorithm type */ import type { IUTXOSelector, SelectionOptions, SelectorAlgorithm, } from '../interfaces/selector.interface.ts'; import type { EnhancedSelectionResult } from '../interfaces/selector-result.interface.ts'; import { SelectionFailureReason } from '../interfaces/selector-result.interface.ts'; import { BaseSelector } from './base-selector.ts'; import { AccumulativeSelector } from './accumulative.ts'; import { BlackjackSelector } from './blackjack.ts'; import { BranchAndBoundSelector } from './branch-and-bound.ts'; import { ConsolidationSelector } from './consolidation-selector.ts'; import { KnapsackSelector } from './knapsack-selector.ts'; import { MockProtectionDetector, ProtectionAwareSelector } from './protection-aware-selector.ts'; import type { IProtectionDetector } from '../interfaces/protection.interface.ts'; import { OutputGroupSelector } from './output-group-selector.ts'; import { SingleRandomDrawSelector } from './single-random-draw-selector.ts'; import { TaxOptimizedSelector, type TaxStrategy, type UTXOTaxMetadata, } from './tax-optimized-selector.ts'; import { WasteOptimizedSelector } from './waste-optimized.ts'; export class SelectorFactory { private static instance: SelectorFactory | null = null; private selectorCache = new Map<string, IUTXOSelector>(); private protectionDetector: IProtectionDetector | null = null; private taxMetadata: UTXOTaxMetadata[] = []; private currentBTCPrice: number = 0; /** * Get singleton instance */ static getInstance(): SelectorFactory { if (!SelectorFactory.instance) { SelectorFactory.instance = new SelectorFactory(); } return SelectorFactory.instance; } /** * Configure protection detector for protection-aware selection */ setProtectionDetector(detector: IProtectionDetector): void { this.protectionDetector = detector; } /** * Configure tax metadata for tax-optimized selection */ setTaxMetadata(metadata: UTXOTaxMetadata[], btcPrice: number): void { this.taxMetadata = metadata; this.currentBTCPrice = btcPrice; } /** * Create selector instance with optional configuration */ create( algorithm: SelectorAlgorithm | string, config?: { protectionDetector?: IProtectionDetector; fallbackSelector?: IUTXOSelector; taxStrategy?: TaxStrategy; taxMetadata?: UTXOTaxMetadata[]; btcPrice?: number; privacyLevel?: 'low' | 'medium' | 'high'; consolidationThreshold?: number; longTermFeeRate?: number; }, ): IUTXOSelector { // Create cache key including config for advanced selectors const cacheKey = this.getCacheKey(algorithm, config); // Return cached instance if available if (this.selectorCache.has(cacheKey)) { return this.selectorCache.get(cacheKey)!; } let selector: IUTXOSelector; switch (algorithm) { case 'accumulative': selector = new AccumulativeSelector(); break; case 'branch-and-bound': selector = new BranchAndBoundSelector(); break; case 'blackjack': selector = new BlackjackSelector(); break; case 'waste-optimized': selector = new WasteOptimizedSelector(); break; case 'knapsack': selector = new KnapsackSelector(); break; case 'single-random-draw': selector = new SingleRandomDrawSelector(); break; case 'output-group': { const fallbackForOutputGroup = config?.fallbackSelector ? config.fallbackSelector as BaseSelector : new BranchAndBoundSelector(); selector = new OutputGroupSelector( config?.privacyLevel || 'medium', fallbackForOutputGroup, ); break; } case 'consolidation': selector = new ConsolidationSelector({ consolidationThreshold: config?.consolidationThreshold, longTermFeeRate: config?.longTermFeeRate, }); break; case 'protection-aware': { const detectorOrMock = config?.protectionDetector || this.protectionDetector || new MockProtectionDetector(); const fallbackSelector = config?.fallbackSelector ? config.fallbackSelector as BaseSelector : new BranchAndBoundSelector(); selector = new ProtectionAwareSelector(detectorOrMock, fallbackSelector); break; } case 'tax-optimized-fifo': case 'tax-optimized-lifo': case 'tax-optimized-hifo': case 'tax-optimized-lofo': { const taxStrategy = algorithm.replace('tax-optimized-', '') .toUpperCase() as TaxStrategy; const taxMetadata = config?.taxMetadata || this.taxMetadata; const currentPrice = config?.btcPrice || this.currentBTCPrice; if (!taxMetadata.length || !currentPrice) { throw new Error( 'Tax-optimized selector requires tax metadata and BTC price', ); } const fallbackSelector = config?.fallbackSelector ? config.fallbackSelector as BaseSelector : new BranchAndBoundSelector(); selector = new TaxOptimizedSelector({ strategy: taxStrategy, taxMetadata, currentBTCPrice: currentPrice, fallbackSelector, }); break; } // Legacy simple implementations for backward compatibility case 'fifo': selector = new FIFOSelector(); break; case 'lifo': selector = new LIFOSelector(); break; default: throw new Error(`Unknown selector algorithm: ${algorithm}`); } // Cache the selector this.selectorCache.set(cacheKey, selector); return selector; } /** * Generate cache key for selector with config */ private getCacheKey(algorithm: string, config?: any): string { if (!config) return algorithm; // Create a simple hash of config for caching const configStr = JSON.stringify(config, (_key, value) => { // Skip functions and complex objects for cache key if ( typeof value === 'function' || (value instanceof Object && value.constructor !== Object) ) { return undefined; } return value; }); return `${algorithm}-${this.simpleHash(configStr)}`; } /** * Simple hash function for cache keys */ private simpleHash(str: string): string { let hash = 0; for (let i = 0; i < str.length; i++) { const char = str.charCodeAt(i); hash = (hash << 5) - hash + char; hash = hash & hash; // Convert to 32bit integer } return hash.toString(36); } /** * Get all available algorithms */ getAvailableAlgorithms(): string[] { return [ // Core algorithms 'accumulative', 'branch-and-bound', 'blackjack', 'waste-optimized', // New advanced algorithms 'knapsack', 'single-random-draw', 'output-group', 'consolidation', 'protection-aware', // Tax-optimized variants 'tax-optimized-fifo', 'tax-optimized-lifo', 'tax-optimized-hifo', 'tax-optimized-lofo', // Legacy simple implementations 'fifo', 'lifo', ]; } /** * Get recommended algorithm based on scenario */ getRecommendedAlgorithm(scenario: { utxoCount: number; targetValue: number; feeRate: number; dustThreshold?: number | undefined; }): SelectorAlgorithm { const { utxoCount, feeRate } = scenario; // For small UTXO sets, use Branch & Bound for optimal selection if (utxoCount <= 20) { return 'branch-and-bound'; } // For exact value matching scenarios, use Blackjack if (this.isLikelyExactMatch(scenario)) { return 'blackjack'; } // For high fee environments, use waste-optimized if (feeRate > 50) { return 'waste-optimized'; } // Default to accumulative for large UTXO sets return 'accumulative'; } /** * Check if scenario is likely to benefit from exact matching */ private isLikelyExactMatch(scenario: { utxoCount: number; targetValue: number; dustThreshold?: number; }): boolean { const { utxoCount, targetValue } = scenario; // Small target values with many UTXOs are good candidates for exact matching return utxoCount > 10 && targetValue < 100000; // 0.001 BTC } /** * Clear selector cache */ clearCache(): void { this.selectorCache.clear(); } /** * Get cache statistics */ private isValidSelectorAlgorithm(key: string): key is SelectorAlgorithm { const validAlgorithms = [ 'accumulative', 'branch-and-bound', 'blackjack', 'waste-optimized', 'knapsack', 'single-random-draw', 'output-group', 'consolidation', 'protection-aware', 'tax-optimized-fifo', 'tax-optimized-lifo', 'tax-optimized-hifo', 'tax-optimized-lofo', 'fifo', 'lifo', ]; return validAlgorithms.includes(key); } getCacheStats(): { size: number; algorithms: SelectorAlgorithm[]; } { return { size: this.selectorCache.size, algorithms: Array.from(this.selectorCache.keys()).filter( this.isValidSelectorAlgorithm, ) as SelectorAlgorithm[], }; } } // Simple legacy selector implementations class FIFOSelector extends AccumulativeSelector { override getName(): string { return 'fifo'; } override select(utxos: UTXO[], options: SelectionOptions) { return this.selectFIFO(utxos, options); } } class LIFOSelector extends AccumulativeSelector { override getName(): string { return 'lifo'; } override select(utxos: UTXO[], options: SelectionOptions): EnhancedSelectionResult { const validationFailure = this.checkOptionsValidity(options); if (validationFailure) return validationFailure; const filteredUTXOs = this.filterEligibleUTXOs(utxos, options); if (filteredUTXOs.length === 0) { return { success: false, reason: SelectionFailureReason.NO_UTXOS_AVAILABLE, message: 'No UTXOs available that meet confirmation requirements', details: { utxoCount: utxos.length, minConfirmations: options.minConfirmations, }, }; } // Sort by confirmations (newest first) const sortedUTXOs = [...filteredUTXOs].sort( (a, b) => (b.confirmations ?? 0) - (a.confirmations ?? 0), ); return this.selectFromSortedLifo(sortedUTXOs, options); } private selectFromSortedLifo( sortedUTXOs: UTXO[], options: SelectionOptions, ): EnhancedSelectionResult { // Use the same logic as AccumulativeSelector but with pre-sorted UTXOs const selected: UTXO[] = []; let accumulated = 0; let estimatedFee = this.estimateFee(1, 2, options.feeRate); let target = options.targetValue + estimatedFee; for (const utxo of sortedUTXOs) { if (options.maxInputs && selected.length >= options.maxInputs) break; selected.push(utxo); accumulated += utxo.value; estimatedFee = this.estimateFee(selected.length, 2, options.feeRate); target = options.targetValue + estimatedFee; if (accumulated >= target) { const change = accumulated - options.targetValue - estimatedFee; const hasChange = change >= (options.dustThreshold ?? this.DUST_THRESHOLD); if (!hasChange) { estimatedFee = this.estimateFee(selected.length, 1, options.feeRate); target = options.targetValue + estimatedFee; } if (accumulated >= target) { return this.createResult( selected, options.targetValue, options.feeRate, hasChange, ); } } } return { success: false, reason: SelectionFailureReason.INSUFFICIENT_FUNDS, message: 'Insufficient funds to meet target value', details: { availableBalance: accumulated, requiredAmount: target, utxoCount: selected.length, }, }; } } // Note: KnapsackSelector implementation is imported from its own file // Note: SingleRandomDrawSelector implementation is imported from its own file // Re-export required types import type { UTXO } from '../interfaces/provider.interface.ts'; // Removing unused function to satisfy linter /** * Default factory instance */ export const selectorFactory = SelectorFactory.getInstance(); /** * Utility function to create selector */ export function createSelector( algorithm: SelectorAlgorithm | string, config?: Parameters<SelectorFactory['create']>[1], ): IUTXOSelector { return selectorFactory.create(algorithm, config); } /** * Utility function to get recommended selector */ export function getRecommendedSelector(scenario: { utxoCount: number; targetValue: number; feeRate: number; dustThreshold?: number; }): IUTXOSelector { const algorithm = selectorFactory.getRecommendedAlgorithm(scenario); return selectorFactory.create(algorithm); } /** * Create protection-aware selector with mock detector */ export function createProtectionAwareSelector( protectedUtxos: string[] = [], fallbackAlgorithm: SelectorAlgorithm = 'branch-and-bound', ): IUTXOSelector { const detector = new MockProtectionDetector(protectedUtxos); const fallback = selectorFactory.create(fallbackAlgorithm) as BaseSelector; return new ProtectionAwareSelector(detector, fallback); } /** * Create tax-optimized selector */ export function createTaxOptimizedSelector( strategy: TaxStrategy, taxMetadata: UTXOTaxMetadata[], btcPrice: number, fallbackAlgorithm?: SelectorAlgorithm, ): IUTXOSelector { return selectorFactory.create(`tax-optimized-${strategy.toLowerCase()}`, { taxMetadata, btcPrice, fallbackSelector: fallbackAlgorithm ? selectorFactory.create(fallbackAlgorithm) : undefined, }); } /** * Create privacy-optimized selector */ export function createPrivacySelector( level: 'low' | 'medium' | 'high' = 'medium', fallbackAlgorithm?: SelectorAlgorithm, ): IUTXOSelector { return selectorFactory.create('output-group', { privacyLevel: level, fallbackSelector: fallbackAlgorithm ? selectorFactory.create(fallbackAlgorithm) : undefined, }); } /** * Create consolidation-optimized selector */ export function createConsolidationSelector( threshold?: number, longTermFeeRate?: number, ): IUTXOSelector { return selectorFactory.create('consolidation', { consolidationThreshold: threshold, longTermFeeRate, }); } // Re-export new selector types for convenience export type { TaxCalculation, TaxStrategy, UTXOTaxMetadata } from './tax-optimized-selector.ts'; export type { OutputGroup } from './output-group-selector.ts'; export type { ConsolidationMetrics } from './consolidation-selector.ts'; export { MockProtectionDetector } from './protection-aware-selector.ts';