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