UNPKG

minimal-xec-wallet

Version:

A minimalist eCash (XEC) wallet npm library, for use in web apps. Supports eTokens.

304 lines (257 loc) 7.86 kB
/* Simplified UTXO management for minimal XEC wallet Core functionality only: - Fetch UTXOs from chronik - Basic validation and filtering - Simple caching - Essential security checks */ const SecurityValidator = require('./security') class Utxos { constructor (localConfig = {}) { this.chronik = localConfig.chronik this.ar = localConfig.ar if (!this.ar) { throw new Error('AdapterRouter instance required for UTXO management') } // Simple UTXO store this.utxoStore = { xecUtxos: [], lastUpdated: null, cacheKey: null } // Security validator this.security = new SecurityValidator(localConfig.security) // Simple configuration this.maxRetries = localConfig.maxRetries || 3 this.retryDelay = localConfig.retryDelay || 1000 this.cacheTimeout = localConfig.cacheTimeout || 30000 // 30 seconds // Performance tracking (basic) this.performanceMetrics = { totalRequests: 0, cacheHits: 0, lastRefreshTime: null, totalResponseTime: 0, averageResponseTime: 0 } } /** * Initialize UTXO store for an address * @param {string} addr - XEC address * @param {boolean} forceRefresh - Force refresh cache * @returns {boolean} - Success status */ async initUtxoStore (addr, forceRefresh = false) { try { this.performanceMetrics.totalRequests++ // Check cache validity if (!forceRefresh && this._isCacheValid(addr)) { this.performanceMetrics.cacheHits++ return true } // Fetch fresh UTXO data const utxosResult = await this._fetchUtxosWithRetry(addr) // Process and store UTXOs this._processUtxos(utxosResult, addr) return true } catch (err) { throw new Error(`UTXO initialization failed: ${err.message}`) } } /** * Get spendable XEC UTXOs with basic filtering * @param {Object} options - Filtering options * @returns {Array} - Filtered UTXOs */ getSpendableXecUtxos (options = {}) { const { includeUnconfirmed = false, excludeDustAttack = true } = options // Use security validator for filtering return this.security.filterSecureUtxos(this.utxoStore.xecUtxos, { includeUnconfirmed, excludeDustAttack }) } /** * Simple UTXO selection (largest first) * @param {number} targetAmount - Target amount in satoshis * @param {Object} options - Selection options * @returns {Object} - Selection result */ selectOptimalUtxos (targetAmount, options = {}) { const spendableUtxos = this.getSpendableXecUtxos(options) if (spendableUtxos.length === 0) { throw new Error('No spendable UTXOs available') } // Sort by value descending (largest first) const sortedUtxos = spendableUtxos.sort((a, b) => { const aValue = this._getUtxoValue(a) const bValue = this._getUtxoValue(b) return bValue - aValue }) // Simple greedy selection const selectedUtxos = [] let totalAmount = 0 const inputCost = 148 // P2PKH input size in bytes for (const utxo of sortedUtxos) { const utxoValue = this._getUtxoValue(utxo) selectedUtxos.push(utxo) totalAmount += utxoValue // Estimate fee const estimatedFee = selectedUtxos.length * inputCost + 34 + 10 // inputs + output + overhead if (totalAmount >= targetAmount + estimatedFee) { break } } // Check if we have enough const finalFee = selectedUtxos.length * inputCost + 34 + 10 if (totalAmount < targetAmount + finalFee) { throw new Error('Insufficient funds') } return { selectedUtxos, totalAmount, estimatedFee: finalFee, change: totalAmount - targetAmount - finalFee } } /** * Get current balance * @returns {Object} - Balance information */ getBalance () { const utxos = this.utxoStore.xecUtxos let confirmed = 0 let unconfirmed = 0 utxos.forEach(utxo => { const value = this._getUtxoValue(utxo) if (utxo.blockHeight === -1) { unconfirmed += value } else { confirmed += value } }) return { confirmed, unconfirmed, total: confirmed + unconfirmed } } /** * Get performance metrics * @returns {Object} - Performance data */ getPerformanceMetrics () { const cacheHitRate = this.performanceMetrics.totalRequests > 0 ? (this.performanceMetrics.cacheHits / this.performanceMetrics.totalRequests) * 100 : 0 return { cacheHitRate: Math.round(cacheHitRate * 100) / 100, totalRequests: this.performanceMetrics.totalRequests, cacheHits: this.performanceMetrics.cacheHits, averageResponseTime: this.performanceMetrics.averageResponseTime, lastRefreshTime: this.performanceMetrics.lastRefreshTime, utxoCount: this.utxoStore.xecUtxos.length } } /** * Clear cache */ clearCache () { this.utxoStore = { xecUtxos: [], lastUpdated: null, cacheKey: null } } /** * Get spendable eToken UTXOs (Phase 2 - not implemented) * @returns {Array} - Empty array for Phase 1 */ getSpendableETokenUtxos () { return [] } /** * Refresh cache for address * @param {string} addr - Address to refresh * @returns {boolean} - Success status */ async refreshCache (addr) { return await this.initUtxoStore(addr, true) } /** * Filter dust UTXOs (legacy method) * @param {Array} utxos - UTXOs to filter * @returns {Array} - Non-dust UTXOs */ _filterDustUtxos (utxos) { return utxos.filter(utxo => this._getUtxoValue(utxo) >= 1000) } /** * Sort UTXOs by value (legacy method) * @param {Array} utxos - UTXOs to sort * @param {string} order - 'asc' or 'desc' * @returns {Array} - Sorted UTXOs */ _sortUtxosByValue (utxos, order = 'desc') { return [...utxos].sort((a, b) => { const aValue = this._getUtxoValue(a) const bValue = this._getUtxoValue(b) return order === 'desc' ? bValue - aValue : aValue - bValue }) } // Private methods async _fetchUtxosWithRetry (addr, maxRetries = null) { const retryLimit = maxRetries || this.maxRetries let attempt = 1 while (attempt <= retryLimit) { try { const utxosResult = await this.ar.getUtxos(addr) return utxosResult } catch (err) { if (attempt === retryLimit) { throw new Error(`Failed to fetch UTXOs after ${retryLimit} attempts: ${err.message}`) } await this._delay(this.retryDelay * attempt) attempt++ } } } _processUtxos (utxosResult, addr) { if (!utxosResult || !Array.isArray(utxosResult.utxos)) { throw new Error('Invalid UTXO response format') } // Filter and validate UTXOs const validUtxos = utxosResult.utxos.filter(utxo => this._isValidUtxo(utxo)) // Store UTXOs this.utxoStore.xecUtxos = validUtxos this.utxoStore.lastUpdated = Date.now() this.utxoStore.cacheKey = addr this.performanceMetrics.lastRefreshTime = Date.now() } _isValidUtxo (utxo) { return this.security.isValidUtxoStructure(utxo) && this._getUtxoValue(utxo) > 0 } _getUtxoValue (utxo) { if (utxo.sats !== undefined) { return typeof utxo.sats === 'bigint' ? Number(utxo.sats) : parseInt(utxo.sats) } if (utxo.value !== undefined) { return typeof utxo.value === 'bigint' ? Number(utxo.value) : parseInt(utxo.value) } return 0 } _isCacheValid (addr) { return ( this.utxoStore.cacheKey === addr && this.utxoStore.lastUpdated && (Date.now() - this.utxoStore.lastUpdated) < this.cacheTimeout ) } _delay (ms) { return new Promise(resolve => setTimeout(resolve, ms)) } } module.exports = Utxos