@xchainjs/xchain-utxo 2.0.9 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -35,6 +35,995 @@ typeof SuppressedError === "function" ? SuppressedError : function (error, suppr
35
35
  return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
36
36
  };
37
37
 
38
+ /**
39
+ * UTXO-specific error codes for better error handling and debugging
40
+ */
41
+ exports.UtxoErrorCode = void 0;
42
+ (function (UtxoErrorCode) {
43
+ UtxoErrorCode["INSUFFICIENT_BALANCE"] = "INSUFFICIENT_BALANCE";
44
+ UtxoErrorCode["INVALID_ADDRESS"] = "INVALID_ADDRESS";
45
+ UtxoErrorCode["INVALID_AMOUNT"] = "INVALID_AMOUNT";
46
+ UtxoErrorCode["INVALID_FEE_RATE"] = "INVALID_FEE_RATE";
47
+ UtxoErrorCode["INVALID_MEMO"] = "INVALID_MEMO";
48
+ UtxoErrorCode["INVALID_UTXO"] = "INVALID_UTXO";
49
+ UtxoErrorCode["PROVIDER_ERROR"] = "PROVIDER_ERROR";
50
+ UtxoErrorCode["NETWORK_ERROR"] = "NETWORK_ERROR";
51
+ UtxoErrorCode["VALIDATION_ERROR"] = "VALIDATION_ERROR";
52
+ UtxoErrorCode["TRANSACTION_TOO_LARGE"] = "TRANSACTION_TOO_LARGE";
53
+ UtxoErrorCode["UTXO_SELECTION_FAILED"] = "UTXO_SELECTION_FAILED";
54
+ UtxoErrorCode["BROADCAST_ERROR"] = "BROADCAST_ERROR";
55
+ UtxoErrorCode["SIGNING_ERROR"] = "SIGNING_ERROR";
56
+ })(exports.UtxoErrorCode || (exports.UtxoErrorCode = {}));
57
+ /**
58
+ * Enhanced error class for UTXO operations with detailed context
59
+ */
60
+ class UtxoError extends Error {
61
+ constructor(code, message, details) {
62
+ super(message);
63
+ this.code = code;
64
+ this.details = details;
65
+ this.isUtxoError = true;
66
+ this.name = 'UtxoError';
67
+ // Ensure proper prototype chain for instanceof checks
68
+ Object.setPrototypeOf(this, UtxoError.prototype);
69
+ }
70
+ /**
71
+ * Create an insufficient balance error
72
+ */
73
+ static insufficientBalance(required, available, chain) {
74
+ return new UtxoError(exports.UtxoErrorCode.INSUFFICIENT_BALANCE, `Insufficient balance: required ${required}, available ${available}`, { required, available, chain });
75
+ }
76
+ /**
77
+ * Create an invalid address error
78
+ */
79
+ static invalidAddress(address, network) {
80
+ return new UtxoError(exports.UtxoErrorCode.INVALID_ADDRESS, `Invalid address: ${address}${network ? ` for network ${network}` : ''}`, { address, network });
81
+ }
82
+ /**
83
+ * Create an invalid amount error
84
+ */
85
+ static invalidAmount(amount, reason) {
86
+ const message = `Invalid amount: ${amount}${reason ? ` (${reason})` : ''}`;
87
+ return new UtxoError(exports.UtxoErrorCode.INVALID_AMOUNT, message, { amount, reason });
88
+ }
89
+ /**
90
+ * Create an invalid fee rate error
91
+ */
92
+ static invalidFeeRate(feeRate, reason) {
93
+ const message = `Invalid fee rate: ${feeRate}${reason ? ` (${reason})` : ''}`;
94
+ return new UtxoError(exports.UtxoErrorCode.INVALID_FEE_RATE, message, { feeRate, reason });
95
+ }
96
+ /**
97
+ * Create an invalid memo error
98
+ */
99
+ static invalidMemo(memo, reason) {
100
+ return new UtxoError(exports.UtxoErrorCode.INVALID_MEMO, `Invalid memo: ${reason}`, { memo, reason });
101
+ }
102
+ /**
103
+ * Create a provider error
104
+ */
105
+ static providerError(providerName, originalError) {
106
+ return new UtxoError(exports.UtxoErrorCode.PROVIDER_ERROR, `Provider ${providerName} error: ${originalError.message}`, {
107
+ providerName,
108
+ originalError: originalError.stack,
109
+ });
110
+ }
111
+ /**
112
+ * Create a network error
113
+ */
114
+ static networkError(operation, originalError) {
115
+ return new UtxoError(exports.UtxoErrorCode.NETWORK_ERROR, `Network error during ${operation}: ${originalError.message}`, {
116
+ operation,
117
+ originalError: originalError.stack,
118
+ });
119
+ }
120
+ /**
121
+ * Create a validation error
122
+ */
123
+ static validationError(message, details) {
124
+ return new UtxoError(exports.UtxoErrorCode.VALIDATION_ERROR, `Validation failed: ${message}`, details);
125
+ }
126
+ /**
127
+ * Create a transaction too large error
128
+ */
129
+ static transactionTooLarge(currentSize, maxSize) {
130
+ return new UtxoError(exports.UtxoErrorCode.TRANSACTION_TOO_LARGE, `Transaction size ${currentSize} bytes exceeds maximum ${maxSize} bytes`, { currentSize, maxSize });
131
+ }
132
+ /**
133
+ * Create a UTXO selection failed error
134
+ */
135
+ static utxoSelectionFailed(targetAmount, availableAmount, strategy) {
136
+ const message = `UTXO selection failed: need ${targetAmount}, have ${availableAmount}${strategy ? ` using ${strategy}` : ''}`;
137
+ return new UtxoError(exports.UtxoErrorCode.UTXO_SELECTION_FAILED, message, { targetAmount, availableAmount, strategy });
138
+ }
139
+ /**
140
+ * Create a broadcast error
141
+ */
142
+ static broadcastError(txHash, originalError) {
143
+ return new UtxoError(exports.UtxoErrorCode.BROADCAST_ERROR, `Failed to broadcast transaction ${txHash}: ${originalError.message}`, { txHash, originalError: originalError.stack });
144
+ }
145
+ /**
146
+ * Create a signing error
147
+ */
148
+ static signingError(reason, details) {
149
+ return new UtxoError(exports.UtxoErrorCode.SIGNING_ERROR, `Transaction signing failed: ${reason}`, details);
150
+ }
151
+ /**
152
+ * Convert unknown errors to typed UTXO errors
153
+ */
154
+ static fromUnknown(error, context) {
155
+ if (error instanceof UtxoError) {
156
+ return error;
157
+ }
158
+ if (error instanceof Error) {
159
+ // Try to categorize common error patterns
160
+ const message = error.message.toLowerCase();
161
+ if (message.includes('insufficient')) {
162
+ return UtxoError.insufficientBalance('unknown', 'unknown');
163
+ }
164
+ if (message.includes('invalid address')) {
165
+ return UtxoError.invalidAddress('unknown');
166
+ }
167
+ if (message.includes('fee') && (message.includes('low') || message.includes('high'))) {
168
+ return UtxoError.invalidFeeRate(0, error.message);
169
+ }
170
+ if (message.includes('network') || message.includes('timeout') || message.includes('connection')) {
171
+ return UtxoError.networkError(context || 'unknown', error);
172
+ }
173
+ return new UtxoError(exports.UtxoErrorCode.VALIDATION_ERROR, context ? `${context}: ${error.message}` : error.message, {
174
+ originalError: error.stack,
175
+ });
176
+ }
177
+ return new UtxoError(exports.UtxoErrorCode.VALIDATION_ERROR, context ? `${context}: ${String(error)}` : String(error), {
178
+ originalError: error,
179
+ });
180
+ }
181
+ /**
182
+ * Check if an error is a UTXO error
183
+ */
184
+ static isUtxoError(error) {
185
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
186
+ return error instanceof Error && error.isUtxoError === true;
187
+ }
188
+ /**
189
+ * Get a user-friendly error message
190
+ */
191
+ getUserFriendlyMessage() {
192
+ switch (this.code) {
193
+ case exports.UtxoErrorCode.INSUFFICIENT_BALANCE:
194
+ return 'Insufficient balance to complete this transaction. Please check your balance and try again.';
195
+ case exports.UtxoErrorCode.INVALID_ADDRESS:
196
+ return 'The provided address is not valid. Please check the address format and network.';
197
+ case exports.UtxoErrorCode.INVALID_AMOUNT:
198
+ return 'The transaction amount is not valid. Amount must be greater than zero.';
199
+ case exports.UtxoErrorCode.INVALID_FEE_RATE:
200
+ return 'The fee rate is not valid. Please provide a fee rate between 1 and 1000 sat/byte.';
201
+ case exports.UtxoErrorCode.INVALID_UTXO:
202
+ return 'One or more UTXOs are invalid or inconsistent. Please refresh UTXOs and try again.';
203
+ case exports.UtxoErrorCode.UTXO_SELECTION_FAILED:
204
+ return 'Could not select UTXOs for this transaction. You may have insufficient funds or too many small UTXOs.';
205
+ case exports.UtxoErrorCode.NETWORK_ERROR:
206
+ return 'Network error occurred. Please check your connection and try again.';
207
+ case exports.UtxoErrorCode.PROVIDER_ERROR:
208
+ return 'Blockchain data provider error. Please try again later.';
209
+ case exports.UtxoErrorCode.TRANSACTION_TOO_LARGE:
210
+ return 'Transaction is too large. Try reducing the number of inputs or splitting into multiple transactions.';
211
+ default:
212
+ return this.message;
213
+ }
214
+ }
215
+ /**
216
+ * Convert to JSON for logging
217
+ */
218
+ toJSON() {
219
+ return {
220
+ name: this.name,
221
+ code: this.code,
222
+ message: this.message,
223
+ details: this.details,
224
+ stack: this.stack,
225
+ };
226
+ }
227
+ }
228
+
229
+ /**
230
+ * Standard UTXO constants used across the library
231
+ */
232
+ /**
233
+ * Bitcoin's standard dust threshold (546 satoshis)
234
+ *
235
+ * This is the minimum value for a UTXO to be considered economically spendable.
236
+ * The value comes from Bitcoin Core's calculation:
237
+ * - P2PKH output size: 34 bytes
238
+ * - Cost to spend: 34 bytes × 3 sat/byte = 102 satoshis
239
+ * - Dust threshold: 102 × 3 = 306 satoshis (theoretical)
240
+ * - Bitcoin Core uses 546 for safety margin and worst-case scenarios
241
+ *
242
+ * Note: Different UTXO chains may have different dust thresholds
243
+ */
244
+ const DUST_THRESHOLD = 546; // satoshis
245
+ /**
246
+ * Transaction size constants for fee calculation
247
+ */
248
+ const TX_SIZE_CONSTANTS = {
249
+ /** Base transaction overhead in virtual bytes */
250
+ BASE_TX_SIZE: 10,
251
+ /** Approximate virtual bytes per P2WPKH input */
252
+ BYTES_PER_INPUT: 68,
253
+ /** Virtual bytes per P2WPKH output */
254
+ BYTES_PER_OUTPUT: 31,
255
+ };
256
+ /**
257
+ * Maximum reasonable amount (21M BTC equivalent in satoshis)
258
+ * Used as a sanity check for transaction amounts
259
+ */
260
+ const MAX_REASONABLE_AMOUNT = '2100000000000000';
261
+ /**
262
+ * Maximum decimal precision for UTXO-based cryptocurrencies
263
+ *
264
+ * Most UTXO chains (Bitcoin, Litecoin, Dogecoin, etc.) use 8 decimal places:
265
+ * - 1 BTC = 100,000,000 satoshis (10^8)
266
+ * - 1 LTC = 100,000,000 litoshi (10^8)
267
+ * - 1 DOGE = 100,000,000 koinu (10^8)
268
+ *
269
+ * This differs from Ethereum tokens which can have up to 18 decimals
270
+ */
271
+ const MAX_UTXO_DECIMALS = 8;
272
+
273
+ /**
274
+ * Comprehensive input validation for UTXO transactions
275
+ */
276
+ class UtxoTransactionValidator {
277
+ /**
278
+ * Validate transaction parameters
279
+ */
280
+ static validateTransferParams(params, feeBounds) {
281
+ const errors = [];
282
+ // Address validation - basic checks only
283
+ // Chain-specific validation should be done by the respective client packages
284
+ try {
285
+ this.validateAddressBasic(params.recipient);
286
+ }
287
+ catch (error) {
288
+ if (error instanceof UtxoError) {
289
+ errors.push(`Invalid recipient address: ${error.message}`);
290
+ }
291
+ }
292
+ if (params.sender) {
293
+ try {
294
+ this.validateAddressBasic(params.sender);
295
+ }
296
+ catch (error) {
297
+ if (error instanceof UtxoError) {
298
+ errors.push(`Invalid sender address: ${error.message}`);
299
+ }
300
+ }
301
+ }
302
+ // Amount validation
303
+ if (!params.amount) {
304
+ errors.push('Amount is required');
305
+ }
306
+ else {
307
+ try {
308
+ const amount = params.amount.amount();
309
+ if (amount.lte(0)) {
310
+ errors.push('Amount must be greater than zero');
311
+ }
312
+ // Check for reasonable maximum (21M BTC equivalent in satoshis)
313
+ if (amount.gt(MAX_REASONABLE_AMOUNT)) {
314
+ errors.push('Amount exceeds maximum possible value');
315
+ }
316
+ // Check for dust amount using the standard UTXO dust threshold
317
+ if (amount.lt(DUST_THRESHOLD)) {
318
+ errors.push(`Amount is below dust threshold (${DUST_THRESHOLD} satoshis)`);
319
+ }
320
+ // Validate decimal precision for UTXO chains
321
+ const decimals = params.amount.decimal;
322
+ if (decimals < 0 || decimals > MAX_UTXO_DECIMALS) {
323
+ errors.push(`Invalid decimal precision: ${decimals} (UTXO chains support max ${MAX_UTXO_DECIMALS} decimals)`);
324
+ }
325
+ }
326
+ catch (error) {
327
+ errors.push(`Invalid amount format: ${error instanceof Error ? error.message : String(error)}`);
328
+ }
329
+ }
330
+ // Fee rate validation
331
+ if (params.feeRate !== undefined) {
332
+ if (!Number.isFinite(params.feeRate)) {
333
+ errors.push('Fee rate must be a finite number');
334
+ }
335
+ else {
336
+ const bounds = feeBounds || { lower: 1, upper: 1000 };
337
+ if (params.feeRate < bounds.lower) {
338
+ errors.push(`Fee rate must be at least ${bounds.lower} sat/byte`);
339
+ }
340
+ else if (params.feeRate > bounds.upper) {
341
+ errors.push(`Fee rate is unreasonably high (max ${bounds.upper} sat/byte)`);
342
+ }
343
+ }
344
+ }
345
+ // Memo validation (TypeScript ensures string type at compile time)
346
+ if (params.memo !== undefined) {
347
+ const memoBytes = Buffer.byteLength(params.memo, 'utf8');
348
+ if (memoBytes > 80) {
349
+ errors.push(`Memo too long: ${memoBytes} bytes (max 80 bytes)`);
350
+ }
351
+ // Check for potentially problematic characters
352
+ if (this.containsControlCharacters(params.memo)) {
353
+ errors.push('Memo contains invalid control characters');
354
+ }
355
+ // Check for null bytes
356
+ if (params.memo.includes('\0')) {
357
+ errors.push('Memo cannot contain null bytes');
358
+ }
359
+ }
360
+ // Wallet index validation
361
+ if (params.walletIndex !== undefined) {
362
+ if (!Number.isInteger(params.walletIndex)) {
363
+ errors.push('Wallet index must be an integer');
364
+ }
365
+ else if (params.walletIndex < 0) {
366
+ errors.push('Wallet index must be non-negative');
367
+ }
368
+ else if (params.walletIndex > 2147483647) {
369
+ errors.push('Wallet index exceeds maximum non-hardened index (2^31-1)');
370
+ }
371
+ }
372
+ if (errors.length > 0) {
373
+ throw UtxoError.validationError(errors.join('; '), {
374
+ validationErrors: errors,
375
+ params: this.sanitizeParamsForLogging(params),
376
+ });
377
+ }
378
+ }
379
+ /**
380
+ * Validate UTXO set for consistency and correctness
381
+ */
382
+ static validateUtxoSet(utxos) {
383
+ if (!Array.isArray(utxos)) {
384
+ throw UtxoError.validationError('UTXOs must be an array');
385
+ }
386
+ const errors = [];
387
+ const seenOutpoints = new Set();
388
+ for (const [index, utxo] of utxos.entries()) {
389
+ const prefix = `UTXO[${index}]`;
390
+ // Validate UTXO structure
391
+ if (!utxo || typeof utxo !== 'object') {
392
+ errors.push(`${prefix}: UTXO must be an object`);
393
+ continue;
394
+ }
395
+ // Validate transaction hash
396
+ if (!utxo.hash) {
397
+ errors.push(`${prefix}: Transaction hash is required`);
398
+ }
399
+ else if (typeof utxo.hash !== 'string') {
400
+ errors.push(`${prefix}: Transaction hash must be a string`);
401
+ }
402
+ else if (!/^[a-fA-F0-9]{64}$/.test(utxo.hash)) {
403
+ errors.push(`${prefix}: Invalid transaction hash format`);
404
+ }
405
+ // Validate output index
406
+ if (utxo.index === undefined || utxo.index === null) {
407
+ errors.push(`${prefix}: Output index is required`);
408
+ }
409
+ else if (!Number.isInteger(utxo.index)) {
410
+ errors.push(`${prefix}: Output index must be an integer`);
411
+ }
412
+ else if (utxo.index < 0) {
413
+ errors.push(`${prefix}: Output index must be non-negative`);
414
+ }
415
+ else if (utxo.index > 4294967295) {
416
+ // uint32 max
417
+ errors.push(`${prefix}: Output index exceeds maximum value`);
418
+ }
419
+ // Check for duplicate UTXOs
420
+ const outpoint = `${utxo.hash}:${utxo.index}`;
421
+ if (seenOutpoints.has(outpoint)) {
422
+ errors.push(`${prefix}: Duplicate UTXO ${outpoint}`);
423
+ }
424
+ seenOutpoints.add(outpoint);
425
+ // Validate value
426
+ if (utxo.value === undefined || utxo.value === null) {
427
+ errors.push(`${prefix}: UTXO value is required`);
428
+ }
429
+ else if (!Number.isInteger(utxo.value)) {
430
+ errors.push(`${prefix}: UTXO value must be an integer`);
431
+ }
432
+ else if (utxo.value <= 0) {
433
+ errors.push(`${prefix}: UTXO value must be positive`);
434
+ }
435
+ else if (utxo.value > 2100000000000000) {
436
+ // 21M BTC in satoshis
437
+ errors.push(`${prefix}: UTXO value exceeds maximum`);
438
+ }
439
+ // Note: height property not available in current UTXO type definition
440
+ // Validate witness UTXO if present
441
+ if (utxo.witnessUtxo) {
442
+ if (typeof utxo.witnessUtxo !== 'object') {
443
+ errors.push(`${prefix}: witnessUtxo must be an object`);
444
+ }
445
+ else {
446
+ if (!Buffer.isBuffer(utxo.witnessUtxo.script)) {
447
+ errors.push(`${prefix}: witnessUtxo script must be a Buffer`);
448
+ }
449
+ if (!Number.isInteger(utxo.witnessUtxo.value) || utxo.witnessUtxo.value !== utxo.value) {
450
+ errors.push(`${prefix}: witnessUtxo value mismatch`);
451
+ }
452
+ }
453
+ }
454
+ // Note: nonWitnessUtxo property not available in current UTXO type definition
455
+ }
456
+ if (errors.length > 0) {
457
+ throw UtxoError.validationError(`UTXO validation failed: ${errors.join('; ')}`, {
458
+ utxoCount: utxos.length,
459
+ validationErrors: errors,
460
+ });
461
+ }
462
+ }
463
+ /**
464
+ * Basic address validation - checks for null/empty and basic format
465
+ * NOTE: Chain-specific validation should be done by the respective client packages
466
+ */
467
+ static validateAddressBasic(address) {
468
+ if (!address) {
469
+ throw UtxoError.validationError('Address cannot be empty');
470
+ }
471
+ if (address.trim() !== address) {
472
+ throw UtxoError.validationError('Address cannot have leading or trailing whitespace');
473
+ }
474
+ // Basic length check - most crypto addresses are between 25-100 characters
475
+ if (address.length < 10 || address.length > 100) {
476
+ throw UtxoError.validationError('Address length is invalid');
477
+ }
478
+ }
479
+ /**
480
+ * Validate fee rate against network conditions
481
+ */
482
+ static validateFeeRate(feeRate, networkConditions) {
483
+ if (!Number.isFinite(feeRate)) {
484
+ throw UtxoError.invalidFeeRate(feeRate, 'Fee rate must be a finite number');
485
+ }
486
+ const conditions = networkConditions || {};
487
+ const minFee = conditions.minFeeRate || 1;
488
+ const maxFee = conditions.maxFeeRate || 1000;
489
+ if (feeRate < minFee) {
490
+ throw UtxoError.invalidFeeRate(feeRate, `Below minimum fee rate ${minFee} sat/byte`);
491
+ }
492
+ if (feeRate > maxFee) {
493
+ throw UtxoError.invalidFeeRate(feeRate, `Above maximum fee rate ${maxFee} sat/byte`);
494
+ }
495
+ // Warn if outside recommended range
496
+ if (conditions.recommendedRange) {
497
+ const [recMin, recMax] = conditions.recommendedRange;
498
+ if (feeRate < recMin || feeRate > recMax) {
499
+ console.warn(`Fee rate ${feeRate} is outside recommended range ${recMin}-${recMax} sat/byte`);
500
+ }
501
+ }
502
+ }
503
+ /**
504
+ * Validate transaction size limits
505
+ */
506
+ static validateTransactionSize(estimatedSize, maxSize = 100000) {
507
+ if (estimatedSize <= 0) {
508
+ throw UtxoError.validationError('Transaction size must be positive');
509
+ }
510
+ if (estimatedSize > maxSize) {
511
+ throw UtxoError.transactionTooLarge(estimatedSize, maxSize);
512
+ }
513
+ // Warn for large transactions
514
+ if (estimatedSize > maxSize * 0.8) {
515
+ console.warn(`Transaction size ${estimatedSize} bytes is approaching limit ${maxSize} bytes`);
516
+ }
517
+ }
518
+ // Private helper methods
519
+ static containsControlCharacters(str) {
520
+ // Check for control characters that might cause issues
521
+ // Ranges: 0x00-0x08, 0x0B, 0x0C, 0x0E-0x1F, 0x7F
522
+ for (let i = 0; i < str.length; i++) {
523
+ const code = str.charCodeAt(i);
524
+ if ((code >= 0x00 && code <= 0x08) || // \x00-\x08
525
+ code === 0x0b || // \x0B (vertical tab)
526
+ code === 0x0c || // \x0C (form feed)
527
+ (code >= 0x0e && code <= 0x1f) || // \x0E-\x1F
528
+ code === 0x7f // \x7F (DEL)
529
+ ) {
530
+ return true;
531
+ }
532
+ }
533
+ return false;
534
+ }
535
+ static sanitizeParamsForLogging(params) {
536
+ // Remove sensitive data from params for safe logging
537
+ const sanitized = Object.assign({}, params);
538
+ if (params.amount) {
539
+ sanitized.amount = params.amount.amount().toString();
540
+ }
541
+ return sanitized;
542
+ }
543
+ }
544
+
545
+ /**
546
+ * Branch and Bound strategy - optimal for minimizing fees and change
547
+ */
548
+ class BranchAndBoundStrategy {
549
+ constructor() {
550
+ this.name = 'BranchAndBound';
551
+ }
552
+ select(utxos, targetValue, feeRate, extraOutputs = 1) {
553
+ // Sort UTXOs by descending value for better branch and bound performance
554
+ const sortedUtxos = [...utxos].sort((a, b) => b.value - a.value);
555
+ // Try to find exact match first (pass targetValue, not target+fee to avoid double counting)
556
+ const exactResult = this.findExactMatch(sortedUtxos, targetValue, feeRate, extraOutputs);
557
+ if (exactResult)
558
+ return exactResult;
559
+ // Run branch and bound algorithm
560
+ return this.branchAndBound(sortedUtxos, targetValue, feeRate, extraOutputs);
561
+ }
562
+ findExactMatch(utxos, targetValue, feeRate, extraOutputs) {
563
+ // Calculate fee for single input (no change output)
564
+ const singleInputFee = this.calculateFee(1, extraOutputs, feeRate);
565
+ const requiredWithFee = targetValue + singleInputFee;
566
+ // Sort by how close they are to the exact target
567
+ const sortedByExactness = [...utxos].sort((a, b) => {
568
+ const aDiff = Math.abs(a.value - requiredWithFee);
569
+ const bDiff = Math.abs(b.value - requiredWithFee);
570
+ return aDiff - bDiff;
571
+ });
572
+ // Look for single UTXO that matches exactly or closely
573
+ for (const utxo of sortedByExactness) {
574
+ const change = utxo.value - requiredWithFee;
575
+ // If change is exactly 0 or less than dust (which we absorb into fee)
576
+ if (change >= 0 && change <= BranchAndBoundStrategy.DUST_THRESHOLD) {
577
+ return {
578
+ inputs: [utxo],
579
+ changeAmount: 0,
580
+ fee: singleInputFee + change, // Absorb dust into fee
581
+ efficiency: 1.0, // Perfect efficiency for exact match
582
+ strategy: this.name,
583
+ };
584
+ }
585
+ // If we have reasonable change (not too much excess)
586
+ if (change > BranchAndBoundStrategy.DUST_THRESHOLD && change < targetValue * 0.1) {
587
+ // Add change output fee
588
+ const feeWithChange = this.calculateFee(1, extraOutputs + 1, feeRate);
589
+ const finalChange = utxo.value - targetValue - feeWithChange;
590
+ if (finalChange > BranchAndBoundStrategy.DUST_THRESHOLD) {
591
+ return {
592
+ inputs: [utxo],
593
+ changeAmount: finalChange,
594
+ fee: feeWithChange,
595
+ efficiency: 0.95, // Very good efficiency
596
+ strategy: this.name,
597
+ };
598
+ }
599
+ }
600
+ }
601
+ return null;
602
+ }
603
+ branchAndBound(utxos, targetValue, feeRate, extraOutputs) {
604
+ const target = targetValue;
605
+ let bestResult = null;
606
+ let tries = 0;
607
+ const search = (index, currentInputs, currentValue, depth) => {
608
+ if (tries++ >= BranchAndBoundStrategy.MAX_TRIES)
609
+ return;
610
+ // Calculate current fee and requirements
611
+ const inputCount = currentInputs.length;
612
+ const outputCount = extraOutputs +
613
+ (currentValue >
614
+ target + this.calculateFee(inputCount, extraOutputs, feeRate) + BranchAndBoundStrategy.DUST_THRESHOLD
615
+ ? 1
616
+ : 0);
617
+ const fee = this.calculateFee(inputCount, outputCount, feeRate);
618
+ const required = target + fee;
619
+ // Check if we have a solution
620
+ if (currentValue >= required) {
621
+ const change = currentValue - required;
622
+ const finalFee = change > BranchAndBoundStrategy.DUST_THRESHOLD ? fee : fee + change;
623
+ const result = {
624
+ inputs: [...currentInputs],
625
+ changeAmount: change > BranchAndBoundStrategy.DUST_THRESHOLD ? change : 0,
626
+ fee: finalFee,
627
+ efficiency: this.calculateEfficiency(currentInputs, target, finalFee, change, change <= BranchAndBoundStrategy.DUST_THRESHOLD),
628
+ strategy: this.name,
629
+ };
630
+ if (!bestResult || result.fee < bestResult.fee) {
631
+ bestResult = result;
632
+ }
633
+ return;
634
+ }
635
+ // Pruning conditions
636
+ if (index >= utxos.length)
637
+ return;
638
+ if (depth > 50)
639
+ return; // Prevent too deep recursion (supports wallets with many UTXOs)
640
+ const remainingValue = utxos.slice(index).reduce((sum, utxo) => sum + utxo.value, 0);
641
+ if (currentValue + remainingValue < required)
642
+ return; // Not enough remaining value
643
+ // Branch 1: Include current UTXO
644
+ search(index + 1, [...currentInputs, utxos[index]], currentValue + utxos[index].value, depth + 1);
645
+ // Branch 2: Exclude current UTXO (if we haven't found a good solution yet)
646
+ if (!bestResult || currentInputs.length < 3) {
647
+ search(index + 1, currentInputs, currentValue, depth + 1);
648
+ }
649
+ };
650
+ search(0, [], 0, 0);
651
+ return bestResult;
652
+ }
653
+ calculateEfficiency(inputs, target, fee, change, changeAbsorbed = false) {
654
+ const totalValue = inputs.reduce((sum, utxo) => sum + utxo.value, 0);
655
+ const wastedValue = changeAbsorbed ? 0 : change > BranchAndBoundStrategy.DUST_THRESHOLD ? 0 : change;
656
+ const efficiency = target / (totalValue + fee + wastedValue);
657
+ return Math.min(1, efficiency);
658
+ }
659
+ /**
660
+ * Calculate estimated transaction fee
661
+ */
662
+ calculateFee(inputCount, outputCount, feeRate) {
663
+ const txSize = BranchAndBoundStrategy.BASE_TX_SIZE +
664
+ inputCount * BranchAndBoundStrategy.BYTES_PER_INPUT +
665
+ outputCount * BranchAndBoundStrategy.BYTES_PER_OUTPUT;
666
+ return Math.ceil(txSize * feeRate);
667
+ }
668
+ }
669
+ BranchAndBoundStrategy.MAX_TRIES = 100000;
670
+ BranchAndBoundStrategy.DUST_THRESHOLD = DUST_THRESHOLD;
671
+ BranchAndBoundStrategy.BYTES_PER_INPUT = TX_SIZE_CONSTANTS.BYTES_PER_INPUT;
672
+ BranchAndBoundStrategy.BYTES_PER_OUTPUT = TX_SIZE_CONSTANTS.BYTES_PER_OUTPUT;
673
+ BranchAndBoundStrategy.BASE_TX_SIZE = TX_SIZE_CONSTANTS.BASE_TX_SIZE;
674
+
675
+ /**
676
+ * Single Random Draw strategy - good for privacy
677
+ */
678
+ class SingleRandomDrawStrategy {
679
+ constructor() {
680
+ this.name = 'SingleRandomDraw';
681
+ }
682
+ select(utxos, targetValue, feeRate, extraOutputs = 1) {
683
+ // Shuffle UTXOs using Fisher-Yates for unbiased randomness
684
+ const shuffledUtxos = [...utxos];
685
+ for (let i = shuffledUtxos.length - 1; i > 0; i--) {
686
+ const j = Math.floor(Math.random() * (i + 1));
687
+ [shuffledUtxos[i], shuffledUtxos[j]] = [shuffledUtxos[j], shuffledUtxos[i]];
688
+ }
689
+ for (const utxo of shuffledUtxos) {
690
+ // Calculate fee without change output first
691
+ const feeNoChange = this.calculateFee(1, extraOutputs, feeRate);
692
+ const requiredNoChange = targetValue + feeNoChange;
693
+ if (utxo.value >= requiredNoChange) {
694
+ const potentialChange = utxo.value - requiredNoChange;
695
+ // Check if change would be above dust threshold
696
+ if (potentialChange > SingleRandomDrawStrategy.DUST_THRESHOLD) {
697
+ // Recalculate fee WITH change output
698
+ const feeWithChange = this.calculateFee(1, extraOutputs + 1, feeRate);
699
+ const requiredWithChange = targetValue + feeWithChange;
700
+ const actualChange = utxo.value - requiredWithChange;
701
+ // Verify change is still above dust after recalculation
702
+ if (actualChange > SingleRandomDrawStrategy.DUST_THRESHOLD) {
703
+ return {
704
+ inputs: [utxo],
705
+ changeAmount: actualChange,
706
+ fee: feeWithChange,
707
+ efficiency: targetValue / utxo.value,
708
+ strategy: this.name,
709
+ };
710
+ }
711
+ }
712
+ // No change output - add dust to fee
713
+ return {
714
+ inputs: [utxo],
715
+ changeAmount: 0,
716
+ fee: feeNoChange + potentialChange,
717
+ efficiency: targetValue / utxo.value,
718
+ strategy: this.name,
719
+ };
720
+ }
721
+ }
722
+ return null;
723
+ }
724
+ /**
725
+ * Calculate estimated transaction fee
726
+ */
727
+ calculateFee(inputCount, outputCount, feeRate) {
728
+ const txSize = SingleRandomDrawStrategy.BASE_TX_SIZE +
729
+ inputCount * SingleRandomDrawStrategy.BYTES_PER_INPUT +
730
+ outputCount * SingleRandomDrawStrategy.BYTES_PER_OUTPUT;
731
+ return Math.ceil(txSize * feeRate);
732
+ }
733
+ }
734
+ SingleRandomDrawStrategy.DUST_THRESHOLD = DUST_THRESHOLD;
735
+ SingleRandomDrawStrategy.BYTES_PER_INPUT = TX_SIZE_CONSTANTS.BYTES_PER_INPUT;
736
+ SingleRandomDrawStrategy.BYTES_PER_OUTPUT = TX_SIZE_CONSTANTS.BYTES_PER_OUTPUT;
737
+ SingleRandomDrawStrategy.BASE_TX_SIZE = TX_SIZE_CONSTANTS.BASE_TX_SIZE;
738
+
739
+ /**
740
+ * Accumulative strategy - simple and reliable fallback
741
+ */
742
+ class AccumulativeStrategy {
743
+ constructor() {
744
+ this.name = 'Accumulative';
745
+ }
746
+ select(utxos, targetValue, feeRate, extraOutputs = 1) {
747
+ // Sort by value descending for faster accumulation
748
+ const sortedUtxos = [...utxos].sort((a, b) => b.value - a.value);
749
+ const selectedInputs = [];
750
+ let currentValue = 0;
751
+ for (const utxo of sortedUtxos) {
752
+ selectedInputs.push(utxo);
753
+ currentValue += utxo.value;
754
+ // First check with no change output (minimum fee)
755
+ const feeNoChange = this.calculateFee(selectedInputs.length, extraOutputs, feeRate);
756
+ const requiredNoChange = targetValue + feeNoChange;
757
+ if (currentValue >= requiredNoChange) {
758
+ const potentialChange = currentValue - requiredNoChange;
759
+ // Check if we need a change output
760
+ if (potentialChange > AccumulativeStrategy.DUST_THRESHOLD) {
761
+ // Recalculate with change output
762
+ const feeWithChange = this.calculateFee(selectedInputs.length, extraOutputs + 1, feeRate);
763
+ const finalChange = currentValue - targetValue - feeWithChange;
764
+ // Verify change is still above dust after recalculation
765
+ if (finalChange > AccumulativeStrategy.DUST_THRESHOLD) {
766
+ return {
767
+ inputs: [...selectedInputs],
768
+ changeAmount: finalChange,
769
+ fee: feeWithChange,
770
+ efficiency: targetValue / currentValue,
771
+ strategy: this.name,
772
+ };
773
+ }
774
+ }
775
+ // No change output - absorb dust into fee
776
+ return {
777
+ inputs: [...selectedInputs],
778
+ changeAmount: 0,
779
+ fee: feeNoChange + potentialChange,
780
+ efficiency: targetValue / currentValue,
781
+ strategy: this.name,
782
+ };
783
+ }
784
+ }
785
+ return null;
786
+ }
787
+ /**
788
+ * Calculate estimated transaction fee
789
+ */
790
+ calculateFee(inputCount, outputCount, feeRate) {
791
+ const txSize = AccumulativeStrategy.BASE_TX_SIZE +
792
+ inputCount * AccumulativeStrategy.BYTES_PER_INPUT +
793
+ outputCount * AccumulativeStrategy.BYTES_PER_OUTPUT;
794
+ return Math.ceil(txSize * feeRate);
795
+ }
796
+ }
797
+ AccumulativeStrategy.DUST_THRESHOLD = DUST_THRESHOLD;
798
+ AccumulativeStrategy.BYTES_PER_INPUT = TX_SIZE_CONSTANTS.BYTES_PER_INPUT;
799
+ AccumulativeStrategy.BYTES_PER_OUTPUT = TX_SIZE_CONSTANTS.BYTES_PER_OUTPUT;
800
+ AccumulativeStrategy.BASE_TX_SIZE = TX_SIZE_CONSTANTS.BASE_TX_SIZE;
801
+
802
+ /**
803
+ * Largest First strategy - good for consolidation
804
+ */
805
+ class LargestFirstStrategy {
806
+ constructor() {
807
+ this.name = 'LargestFirst';
808
+ }
809
+ select(utxos, targetValue, feeRate, extraOutputs = 1) {
810
+ // Sort by value descending
811
+ const sortedUtxos = [...utxos].sort((a, b) => b.value - a.value);
812
+ return new AccumulativeStrategy().select(sortedUtxos, targetValue, feeRate, extraOutputs);
813
+ }
814
+ }
815
+
816
+ /**
817
+ * Small First strategy - good for consolidating many small UTXOs
818
+ */
819
+ class SmallFirstStrategy {
820
+ constructor() {
821
+ this.name = 'SmallFirst';
822
+ }
823
+ select(utxos, targetValue, feeRate, extraOutputs = 1) {
824
+ // Sort by value ascending to prioritize small UTXOs
825
+ const sortedUtxos = [...utxos].sort((a, b) => a.value - b.value);
826
+ const selectedInputs = [];
827
+ let currentValue = 0;
828
+ for (const utxo of sortedUtxos) {
829
+ selectedInputs.push(utxo);
830
+ currentValue += utxo.value;
831
+ const fee = this.calculateFee(selectedInputs.length, extraOutputs + 1, feeRate); // Assume change output
832
+ const required = targetValue + fee;
833
+ if (currentValue >= required) {
834
+ const change = currentValue - required;
835
+ const hasChange = change > SmallFirstStrategy.DUST_THRESHOLD;
836
+ const finalOutputs = hasChange ? extraOutputs + 1 : extraOutputs;
837
+ const finalFee = this.calculateFee(selectedInputs.length, finalOutputs, feeRate);
838
+ const finalChange = currentValue - targetValue - finalFee;
839
+ return {
840
+ inputs: [...selectedInputs],
841
+ changeAmount: finalChange > SmallFirstStrategy.DUST_THRESHOLD ? finalChange : 0,
842
+ fee: finalChange > SmallFirstStrategy.DUST_THRESHOLD ? finalFee : finalFee + finalChange,
843
+ efficiency: targetValue / (currentValue + finalFee),
844
+ strategy: this.name,
845
+ };
846
+ }
847
+ }
848
+ return null;
849
+ }
850
+ /**
851
+ * Calculate estimated transaction fee
852
+ */
853
+ calculateFee(inputCount, outputCount, feeRate) {
854
+ const txSize = SmallFirstStrategy.BASE_TX_SIZE +
855
+ inputCount * SmallFirstStrategy.BYTES_PER_INPUT +
856
+ outputCount * SmallFirstStrategy.BYTES_PER_OUTPUT;
857
+ return Math.ceil(txSize * feeRate);
858
+ }
859
+ }
860
+ SmallFirstStrategy.DUST_THRESHOLD = DUST_THRESHOLD;
861
+ SmallFirstStrategy.BYTES_PER_INPUT = TX_SIZE_CONSTANTS.BYTES_PER_INPUT;
862
+ SmallFirstStrategy.BYTES_PER_OUTPUT = TX_SIZE_CONSTANTS.BYTES_PER_OUTPUT;
863
+ SmallFirstStrategy.BASE_TX_SIZE = TX_SIZE_CONSTANTS.BASE_TX_SIZE;
864
+
865
+ /**
866
+ * Enhanced UTXO selector with multiple strategies
867
+ */
868
+ class UtxoSelector {
869
+ constructor() {
870
+ this.strategies = [
871
+ new BranchAndBoundStrategy(),
872
+ new SingleRandomDrawStrategy(),
873
+ new AccumulativeStrategy(),
874
+ new LargestFirstStrategy(),
875
+ new SmallFirstStrategy(),
876
+ ];
877
+ }
878
+ /**
879
+ * Select optimal UTXOs for a transaction
880
+ */
881
+ selectOptimal(utxos, targetValue, feeRate, preferences = {}, extraOutputs = 1) {
882
+ if (utxos.length === 0) {
883
+ throw UtxoError.utxoSelectionFailed(targetValue, 0, 'no UTXOs available');
884
+ }
885
+ // Validate inputs
886
+ this.validateInputs(utxos, targetValue, feeRate);
887
+ // Filter out dust UTXOs if requested
888
+ const filteredUtxos = preferences.avoidDust
889
+ ? utxos.filter((utxo) => utxo.value >= UtxoSelector.DUST_THRESHOLD)
890
+ : utxos;
891
+ if (filteredUtxos.length === 0) {
892
+ throw UtxoError.utxoSelectionFailed(targetValue, 0, 'no non-dust UTXOs available');
893
+ }
894
+ // Try each strategy and collect results
895
+ const results = [];
896
+ for (const strategy of this.strategies) {
897
+ try {
898
+ const result = strategy.select(filteredUtxos, targetValue, feeRate, extraOutputs);
899
+ if (result && this.isValidResult(result, targetValue)) {
900
+ results.push(result);
901
+ }
902
+ }
903
+ catch (_a) {
904
+ // Strategy failed, continue to next one
905
+ }
906
+ }
907
+ if (results.length === 0) {
908
+ const totalValue = filteredUtxos.reduce((sum, utxo) => sum + utxo.value, 0);
909
+ throw UtxoError.utxoSelectionFailed(targetValue, totalValue);
910
+ }
911
+ // Select the best result based on preferences
912
+ return this.selectBestResult(results, preferences);
913
+ }
914
+ /**
915
+ * Select the best result based on preferences
916
+ */
917
+ selectBestResult(results, preferences) {
918
+ return results.reduce((best, current) => {
919
+ const bestScore = this.calculateScore(best, preferences);
920
+ const currentScore = this.calculateScore(current, preferences);
921
+ return currentScore > bestScore ? current : best;
922
+ });
923
+ }
924
+ /**
925
+ * Calculate a score for a result based on preferences
926
+ */
927
+ calculateScore(result, preferences) {
928
+ // Adjust base efficiency weight based on active preferences
929
+ const hasSpecialPreference = preferences.minimizeInputs || preferences.minimizeChange;
930
+ let score = result.efficiency * (hasSpecialPreference ? 0.1 : 0.3);
931
+ // Minimize fee preference
932
+ if (preferences.minimizeFee) {
933
+ const feeScore = Math.max(0, 1 - result.fee / 100000); // Normalize fee
934
+ score += feeScore * 0.3;
935
+ }
936
+ // Minimize inputs preference (privacy and transaction size)
937
+ if (preferences.minimizeInputs) {
938
+ if (result.inputs.length === 1) {
939
+ score += 0.8; // Very high bonus for single input
940
+ }
941
+ else if (result.inputs.length === 2) {
942
+ score += 0.3; // Moderate bonus for 2 inputs
943
+ }
944
+ else if (result.inputs.length === 3) {
945
+ score += 0.1; // Small bonus for 3 inputs
946
+ }
947
+ else {
948
+ // Heavily penalize many inputs when minimizeInputs is requested
949
+ const inputPenalty = Math.min(0.7, (result.inputs.length - 1) * 0.2);
950
+ score -= inputPenalty;
951
+ }
952
+ }
953
+ // Minimize change preference (exact or minimal change)
954
+ if (preferences.minimizeChange) {
955
+ if (result.changeAmount === 0) {
956
+ score += 0.8; // Perfect - no change needed (very high bonus)
957
+ }
958
+ else if (result.changeAmount < UtxoSelector.DUST_THRESHOLD) {
959
+ score += 0.6; // Small change that might be added to fee
960
+ }
961
+ else if (result.changeAmount < 1000) {
962
+ score += 0.4; // Very small change (< 1000 sats)
963
+ }
964
+ else if (result.changeAmount < 5000) {
965
+ score += 0.1; // Small change (< 5000 sats)
966
+ }
967
+ else {
968
+ // Heavily penalize large change amounts when minimizeChange is requested
969
+ // Use much stronger penalty for larger change amounts
970
+ const changePenalty = Math.min(0.9, result.changeAmount / 20000); // Very strong penalty
971
+ score -= changePenalty;
972
+ }
973
+ }
974
+ // Consolidate small UTXOs preference
975
+ if (preferences.consolidateSmallUtxos) {
976
+ const smallUtxoCount = result.inputs.filter((utxo) => utxo.value < 10000).length;
977
+ if (smallUtxoCount > 0) {
978
+ score += smallUtxoCount * 0.2; // Large bonus for each small UTXO consolidated
979
+ }
980
+ // Additional bonus for using multiple small UTXOs instead of one large one
981
+ if (smallUtxoCount >= 3) {
982
+ score += 0.3; // Extra bonus for consolidating 3+ small UTXOs
983
+ }
984
+ }
985
+ return Math.max(0, Math.min(1, score));
986
+ }
987
+ /**
988
+ * Validate inputs for UTXO selection
989
+ */
990
+ validateInputs(utxos, targetValue, feeRate) {
991
+ if (targetValue <= 0) {
992
+ throw UtxoError.invalidAmount(targetValue, 'Target value must be positive');
993
+ }
994
+ if (feeRate <= 0) {
995
+ throw UtxoError.invalidFeeRate(feeRate, 'Fee rate must be positive');
996
+ }
997
+ const totalValue = utxos.reduce((sum, utxo) => sum + utxo.value, 0);
998
+ if (totalValue < targetValue) {
999
+ throw UtxoError.insufficientBalance(targetValue.toString(), totalValue.toString());
1000
+ }
1001
+ }
1002
+ /**
1003
+ * Validate that a result is correct
1004
+ */
1005
+ isValidResult(result, targetValue) {
1006
+ const inputSum = result.inputs.reduce((sum, utxo) => sum + utxo.value, 0);
1007
+ const expectedTotal = targetValue + result.fee + result.changeAmount;
1008
+ // Allow for small rounding differences
1009
+ return Math.abs(inputSum - expectedTotal) <= 1;
1010
+ }
1011
+ /**
1012
+ * Calculate estimated transaction fee
1013
+ */
1014
+ static calculateFee(inputCount, outputCount, feeRate) {
1015
+ const txSize = UtxoSelector.BASE_TX_SIZE +
1016
+ inputCount * UtxoSelector.BYTES_PER_INPUT +
1017
+ outputCount * UtxoSelector.BYTES_PER_OUTPUT;
1018
+ return Math.ceil(txSize * feeRate);
1019
+ }
1020
+ }
1021
+ // Constants for calculations - re-exported from shared constants
1022
+ UtxoSelector.DUST_THRESHOLD = DUST_THRESHOLD;
1023
+ UtxoSelector.BYTES_PER_INPUT = TX_SIZE_CONSTANTS.BYTES_PER_INPUT;
1024
+ UtxoSelector.BYTES_PER_OUTPUT = TX_SIZE_CONSTANTS.BYTES_PER_OUTPUT;
1025
+ UtxoSelector.BASE_TX_SIZE = TX_SIZE_CONSTANTS.BASE_TX_SIZE;
1026
+
38
1027
  /**
39
1028
  * Abstract base class for creating blockchain clients in the UTXO model.
40
1029
  */
@@ -124,7 +1113,6 @@ class Client extends xchainClient.BaseXChainClient {
124
1113
  // see changes for `xchain-bitcoin` https://github.com/xchainjs/xchainjs-lib/pull/490
125
1114
  getBalance(address, _assets /* not used */, _confirmedOnly) {
126
1115
  return __awaiter(this, void 0, void 0, function* () {
127
- // The actual logic for getting balances
128
1116
  return yield this.roundRobinGetBalance(address);
129
1117
  });
130
1118
  }
@@ -190,7 +1178,7 @@ class Client extends xchainClient.BaseXChainClient {
190
1178
  const feeRates = yield this.roundRobinGetFeeRates();
191
1179
  return feeRates;
192
1180
  }
193
- catch (_error) {
1181
+ catch (_a) {
194
1182
  console.warn('Can not retrieve fee rates from provider');
195
1183
  }
196
1184
  }
@@ -199,7 +1187,7 @@ class Client extends xchainClient.BaseXChainClient {
199
1187
  const feeRate = yield this.getFeeRateFromThorchain();
200
1188
  return xchainClient.standardFeeRates(feeRate);
201
1189
  }
202
- catch (_error) {
1190
+ catch (_b) {
203
1191
  console.warn(`Can not retrieve fee rates from Thorchain`);
204
1192
  }
205
1193
  }
@@ -342,6 +1330,219 @@ class Client extends xchainClient.BaseXChainClient {
342
1330
  throw Error('no provider able to BroadcastTx');
343
1331
  });
344
1332
  }
1333
+ // ==================== Enhanced UTXO Selection Methods ====================
1334
+ /**
1335
+ * Enhanced UTXO selection using the UtxoSelector with multiple strategies.
1336
+ * This method is available to all UTXO chain implementations.
1337
+ *
1338
+ * @param utxos Available UTXOs to select from
1339
+ * @param targetValue Target amount in satoshis
1340
+ * @param feeRate Fee rate in satoshis per byte
1341
+ * @param extraOutputs Number of extra outputs (default: 2 for recipient + change)
1342
+ * @param preferences Selection preferences
1343
+ * @returns Selection result with inputs, change, and fee
1344
+ */
1345
+ selectUtxosForTransaction(utxos, targetValue, feeRate, extraOutputs = 2, preferences) {
1346
+ const selector = new UtxoSelector();
1347
+ const defaultPreferences = Object.assign({ minimizeFee: true, minimizeInputs: true, avoidDust: true }, preferences);
1348
+ try {
1349
+ return selector.selectOptimal(utxos, targetValue, feeRate, defaultPreferences, extraOutputs);
1350
+ }
1351
+ catch (error) {
1352
+ if (UtxoError.isUtxoError(error)) {
1353
+ throw error;
1354
+ }
1355
+ throw UtxoError.fromUnknown(error, 'UTXO selection');
1356
+ }
1357
+ }
1358
+ /**
1359
+ * Validate transaction inputs using comprehensive validation.
1360
+ * Chain implementations can override to add chain-specific validation.
1361
+ *
1362
+ * @param params Transaction parameters including sender and feeRate
1363
+ */
1364
+ validateTransactionInputs(params) {
1365
+ // Use comprehensive validator
1366
+ UtxoTransactionValidator.validateTransferParams(params, this.feeBounds);
1367
+ // Chain-specific address validation (uses abstract validateAddress)
1368
+ if (!this.validateAddress(params.recipient)) {
1369
+ throw UtxoError.invalidAddress(params.recipient, this.network);
1370
+ }
1371
+ if (params.sender && !this.validateAddress(params.sender)) {
1372
+ throw UtxoError.invalidAddress(params.sender, this.network);
1373
+ }
1374
+ }
1375
+ /**
1376
+ * Calculate maximum sendable amount by using ALL available UTXOs.
1377
+ *
1378
+ * For a true sweep operation, we use all UTXOs to maximize the sent amount.
1379
+ * This is simpler and more correct than binary search with UTXO selection.
1380
+ *
1381
+ * @param utxos Available UTXOs
1382
+ * @param feeRate Fee rate in satoshis per byte
1383
+ * @param hasMemo Whether transaction has a memo
1384
+ * @param preferences UTXO selection preferences
1385
+ * @returns Maximum sendable amount, fee, and selected inputs
1386
+ */
1387
+ calculateMaxSendableAmount(utxos, feeRate, hasMemo, preferences) {
1388
+ // For sweep, use ALL UTXOs (optionally filter dust if preference set)
1389
+ const inputUtxos = (preferences === null || preferences === void 0 ? void 0 : preferences.avoidDust)
1390
+ ? utxos.filter((utxo) => utxo.value >= UtxoSelector.DUST_THRESHOLD)
1391
+ : utxos;
1392
+ if (inputUtxos.length === 0) {
1393
+ throw UtxoError.insufficientBalance('max', '0', this.chain);
1394
+ }
1395
+ // Calculate total value of all inputs
1396
+ const totalValue = inputUtxos.reduce((sum, utxo) => sum + utxo.value, 0);
1397
+ // For max send: 1 output (recipient) + optional memo output, NO change output
1398
+ const outputCount = hasMemo ? 2 : 1;
1399
+ // Calculate fee for using all inputs
1400
+ const fee = UtxoSelector.calculateFee(inputUtxos.length, outputCount, feeRate);
1401
+ // Maximum sendable is total minus fee
1402
+ const maxAmount = totalValue - fee;
1403
+ if (maxAmount <= 0) {
1404
+ throw UtxoError.insufficientBalance('max', totalValue.toString(), this.chain);
1405
+ }
1406
+ // Verify amount is above dust threshold
1407
+ if (maxAmount < UtxoSelector.DUST_THRESHOLD) {
1408
+ throw UtxoError.invalidAmount(maxAmount, `below dust threshold of ${UtxoSelector.DUST_THRESHOLD} satoshis`);
1409
+ }
1410
+ return {
1411
+ amount: maxAmount,
1412
+ fee,
1413
+ inputs: inputUtxos,
1414
+ };
1415
+ }
1416
+ /**
1417
+ * Get and validate UTXOs for transaction building.
1418
+ *
1419
+ * @param sender Sender address
1420
+ * @param confirmedOnly Whether to only use confirmed UTXOs
1421
+ * @returns Validated UTXO array
1422
+ */
1423
+ getValidatedUtxos(sender, confirmedOnly) {
1424
+ return __awaiter(this, void 0, void 0, function* () {
1425
+ const utxos = yield this.scanUTXOs(sender, confirmedOnly);
1426
+ if (utxos.length === 0) {
1427
+ throw UtxoError.insufficientBalance('any', '0', this.chain);
1428
+ }
1429
+ // Validate UTXO set integrity
1430
+ UtxoTransactionValidator.validateUtxoSet(utxos);
1431
+ return utxos;
1432
+ });
1433
+ }
1434
+ /**
1435
+ * Prepare transaction with enhanced UTXO selection.
1436
+ * Chain implementations should override buildTxPsbt to provide chain-specific PSBT construction.
1437
+ *
1438
+ * @param params Transaction parameters
1439
+ * @returns Prepared transaction with UTXO details
1440
+ */
1441
+ prepareTxEnhanced(_a) {
1442
+ return __awaiter(this, arguments, void 0, function* ({ sender, memo, amount, recipient, spendPendingUTXO = true, feeRate, utxoSelectionPreferences, }) {
1443
+ try {
1444
+ // Comprehensive input validation
1445
+ this.validateTransactionInputs({
1446
+ amount,
1447
+ recipient,
1448
+ memo,
1449
+ sender,
1450
+ feeRate,
1451
+ });
1452
+ // Get validated UTXOs
1453
+ const confirmedOnly = !spendPendingUTXO;
1454
+ const utxos = yield this.getValidatedUtxos(sender, confirmedOnly);
1455
+ const compiledMemo = memo ? this.compileMemo(memo) : null;
1456
+ const targetValue = amount.amount().toNumber();
1457
+ // Output count: recipient + change + optional memo
1458
+ const extraOutputs = 2 + (compiledMemo ? 1 : 0);
1459
+ // Enhanced UTXO selection
1460
+ const selectionResult = this.selectUtxosForTransaction(utxos, targetValue, Math.ceil(feeRate), extraOutputs, utxoSelectionPreferences);
1461
+ // Build PSBT using chain-specific implementation
1462
+ const rawUnsignedTx = yield this.buildTxPsbt({
1463
+ inputs: selectionResult.inputs,
1464
+ recipient,
1465
+ amount: targetValue,
1466
+ changeAmount: selectionResult.changeAmount,
1467
+ changeAddress: sender,
1468
+ memo: compiledMemo,
1469
+ });
1470
+ return { rawUnsignedTx, utxos, inputs: selectionResult.inputs };
1471
+ }
1472
+ catch (error) {
1473
+ if (UtxoError.isUtxoError(error)) {
1474
+ throw error;
1475
+ }
1476
+ throw UtxoError.fromUnknown(error, 'prepareTxEnhanced');
1477
+ }
1478
+ });
1479
+ }
1480
+ /**
1481
+ * Prepare maximum amount transfer (sweep transaction).
1482
+ *
1483
+ * @param params Send max parameters
1484
+ * @returns Prepared transaction with maximum sendable amount
1485
+ */
1486
+ prepareMaxTx(_a) {
1487
+ return __awaiter(this, arguments, void 0, function* ({ sender, recipient, memo, feeRate, spendPendingUTXO = true, utxoSelectionPreferences, }) {
1488
+ try {
1489
+ // Basic validation
1490
+ if (!(recipient === null || recipient === void 0 ? void 0 : recipient.trim())) {
1491
+ throw UtxoError.invalidAddress(recipient, this.network);
1492
+ }
1493
+ if (!this.validateAddress(recipient)) {
1494
+ throw UtxoError.invalidAddress(recipient, this.network);
1495
+ }
1496
+ if (!this.validateAddress(sender)) {
1497
+ throw UtxoError.invalidAddress(sender, this.network);
1498
+ }
1499
+ // Validate fee rate
1500
+ if (typeof feeRate !== 'number' || !isFinite(feeRate) || feeRate <= 0) {
1501
+ throw UtxoError.invalidFeeRate(feeRate, 'Fee rate must be a positive finite number');
1502
+ }
1503
+ const validatedFeeRate = Math.ceil(feeRate);
1504
+ // Get validated UTXOs
1505
+ const confirmedOnly = !spendPendingUTXO;
1506
+ const utxos = yield this.getValidatedUtxos(sender, confirmedOnly);
1507
+ // Calculate maximum sendable amount
1508
+ const maxCalc = this.calculateMaxSendableAmount(utxos, validatedFeeRate, !!memo, utxoSelectionPreferences);
1509
+ const compiledMemo = memo ? this.compileMemo(memo) : null;
1510
+ // Build PSBT using chain-specific implementation (no change for max send)
1511
+ const rawUnsignedTx = yield this.buildTxPsbt({
1512
+ inputs: maxCalc.inputs,
1513
+ recipient,
1514
+ amount: maxCalc.amount,
1515
+ changeAmount: 0,
1516
+ changeAddress: sender,
1517
+ memo: compiledMemo,
1518
+ });
1519
+ return {
1520
+ rawUnsignedTx,
1521
+ utxos,
1522
+ inputs: maxCalc.inputs,
1523
+ maxAmount: maxCalc.amount,
1524
+ fee: maxCalc.fee,
1525
+ };
1526
+ }
1527
+ catch (error) {
1528
+ if (UtxoError.isUtxoError(error)) {
1529
+ throw error;
1530
+ }
1531
+ throw UtxoError.fromUnknown(error, 'prepareMaxTx');
1532
+ }
1533
+ });
1534
+ }
1535
+ /**
1536
+ * Build a PSBT for this chain.
1537
+ * Chain implementations should override this to provide chain-specific PSBT construction.
1538
+ * Default implementation throws - override in chain client to enable enhanced methods.
1539
+ *
1540
+ * @param params PSBT build parameters
1541
+ * @returns Base64-encoded PSBT string
1542
+ */
1543
+ buildTxPsbt(_params) {
1544
+ throw new Error(`buildTxPsbt not implemented for ${this.chain}. Override in chain client to use enhanced methods.`);
1545
+ }
345
1546
  /**
346
1547
  * Retrieves fee rates using a round-robin approach across multiple data providers.
347
1548
  * @returns {Promise<FeeRates>} The fee rates (average, fast, fastest) in `Satoshis/byte`.
@@ -650,5 +1851,17 @@ function toBitcoinJS(chain, network) {
650
1851
  return toBitcoinJSInner(config);
651
1852
  }
652
1853
 
1854
+ exports.AccumulativeStrategy = AccumulativeStrategy;
1855
+ exports.BranchAndBoundStrategy = BranchAndBoundStrategy;
653
1856
  exports.Client = Client;
1857
+ exports.DUST_THRESHOLD = DUST_THRESHOLD;
1858
+ exports.LargestFirstStrategy = LargestFirstStrategy;
1859
+ exports.MAX_REASONABLE_AMOUNT = MAX_REASONABLE_AMOUNT;
1860
+ exports.MAX_UTXO_DECIMALS = MAX_UTXO_DECIMALS;
1861
+ exports.SingleRandomDrawStrategy = SingleRandomDrawStrategy;
1862
+ exports.SmallFirstStrategy = SmallFirstStrategy;
1863
+ exports.TX_SIZE_CONSTANTS = TX_SIZE_CONSTANTS;
1864
+ exports.UtxoError = UtxoError;
1865
+ exports.UtxoSelector = UtxoSelector;
1866
+ exports.UtxoTransactionValidator = UtxoTransactionValidator;
654
1867
  exports.toBitcoinJS = toBitcoinJS;