@xchainjs/xchain-bitcoin 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/client.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { AssetInfo, FeeRate } from '@xchainjs/xchain-client';
2
2
  import { Address } from '@xchainjs/xchain-util';
3
- import { Client as UTXOClient, PreparedTx, TxParams, UTXO, UtxoClientParams } from '@xchainjs/xchain-utxo';
3
+ import { Client as UTXOClient, PreparedTx, TxParams, UTXO, UtxoClientParams, UtxoSelectionPreferences } from '@xchainjs/xchain-utxo';
4
4
  import * as Bitcoin from 'bitcoinjs-lib';
5
5
  import { AddressFormat } from './types';
6
6
  export declare const defaultBTCParams: UtxoClientParams;
@@ -43,9 +43,25 @@ declare abstract class Client extends UTXOClient {
43
43
  */
44
44
  protected getFeeFromUtxos(inputs: UTXO[], feeRate: FeeRate, data?: Buffer | null): number;
45
45
  /**
46
- * Build a Bitcoin transaction.*
46
+ * Enhanced Bitcoin transaction builder with comprehensive validation and optimal UTXO selection
47
+ * @param params Transaction parameters
48
+ * @returns Enhanced transaction build result with PSBT, UTXOs, and inputs
49
+ */
50
+ buildTxEnhanced({ amount, recipient, memo, feeRate, sender, spendPendingUTXO, utxoSelectionPreferences, selectedUtxos, }: TxParams & {
51
+ feeRate: FeeRate;
52
+ sender: Address;
53
+ spendPendingUTXO?: boolean;
54
+ utxoSelectionPreferences?: UtxoSelectionPreferences;
55
+ selectedUtxos?: UTXO[];
56
+ }): Promise<{
57
+ psbt: Bitcoin.Psbt;
58
+ utxos: UTXO[];
59
+ inputs: UTXO[];
60
+ }>;
61
+ /**
62
+ * Build a Bitcoin transaction with enhanced validation and performance.
63
+ * Now uses the enhanced logic internally while maintaining the same API.
47
64
  * @param param0
48
- * @deprecated
49
65
  */
50
66
  buildTx({ amount, recipient, memo, feeRate, sender, spendPendingUTXO, }: TxParams & {
51
67
  feeRate: FeeRate;
@@ -58,8 +74,60 @@ declare abstract class Client extends UTXOClient {
58
74
  inputs: UTXO[];
59
75
  }>;
60
76
  /**
61
- * Prepare transfer.
77
+ * Send maximum possible amount (sweep) with optimal fee calculation
78
+ * @param params Send max parameters
79
+ * @returns Transaction details with maximum sendable amount
80
+ */
81
+ sendMax({ sender, recipient, memo, feeRate, spendPendingUTXO, utxoSelectionPreferences, selectedUtxos, }: {
82
+ sender: Address;
83
+ recipient: Address;
84
+ memo?: string;
85
+ feeRate: FeeRate;
86
+ spendPendingUTXO?: boolean;
87
+ utxoSelectionPreferences?: UtxoSelectionPreferences;
88
+ selectedUtxos?: UTXO[];
89
+ }): Promise<{
90
+ psbt: Bitcoin.Psbt;
91
+ utxos: UTXO[];
92
+ inputs: UTXO[];
93
+ maxAmount: number;
94
+ fee: number;
95
+ }>;
96
+ /**
97
+ * Prepare maximum amount transfer (sweep transaction)
98
+ * @param params Send max parameters
99
+ * @returns Prepared transaction with maximum sendable amount
100
+ */
101
+ prepareMaxTx({ sender, recipient, memo, feeRate, spendPendingUTXO, utxoSelectionPreferences, selectedUtxos, }: {
102
+ sender: Address;
103
+ recipient: Address;
104
+ memo?: string;
105
+ feeRate: FeeRate;
106
+ spendPendingUTXO?: boolean;
107
+ utxoSelectionPreferences?: UtxoSelectionPreferences;
108
+ selectedUtxos?: UTXO[];
109
+ }): Promise<PreparedTx & {
110
+ maxAmount: number;
111
+ fee: number;
112
+ }>;
113
+ /**
114
+ * Enhanced prepare transfer with comprehensive validation and optimal UTXO selection.
115
+ *
116
+ * @param params The transfer options with enhanced UTXO selection preferences.
117
+ * @returns The raw unsigned transaction with enhanced error handling.
118
+ */
119
+ prepareTxEnhanced({ sender, memo, amount, recipient, spendPendingUTXO, feeRate, utxoSelectionPreferences, selectedUtxos, }: TxParams & {
120
+ sender: Address;
121
+ feeRate: FeeRate;
122
+ spendPendingUTXO?: boolean;
123
+ utxoSelectionPreferences?: UtxoSelectionPreferences;
124
+ selectedUtxos?: UTXO[];
125
+ }): Promise<PreparedTx>;
126
+ /**
127
+ * Prepare transfer with enhanced validation and performance.
128
+ * Now uses the enhanced logic internally while maintaining the same API.
62
129
  *
130
+ * @deprecated Use `prepareTxEnhanced` directly for explicit enhanced UTXO selection.
63
131
  * @param {TxParams&Address&FeeRate&boolean} params The transfer options.
64
132
  * @returns {PreparedTx} The raw unsigned transaction.
65
133
  */
@@ -1,6 +1,6 @@
1
1
  import { FeeRate, TxHash } from '@xchainjs/xchain-client';
2
2
  import { Address } from '@xchainjs/xchain-util';
3
- import { TxParams, UtxoClientParams } from '@xchainjs/xchain-utxo';
3
+ import { TxParams, UTXO, UtxoClientParams, UtxoSelectionPreferences } from '@xchainjs/xchain-utxo';
4
4
  import { Client } from './client';
5
5
  import { AddressFormat } from './types';
6
6
  /**
@@ -41,11 +41,37 @@ declare class ClientKeystore extends Client {
41
41
  * Transfer BTC.
42
42
  *
43
43
  * @param {TxParams&FeeRate} params The transfer options including the fee rate.
44
- * @returns {Promise<TxHash|string>} A promise that resolves to the transaction hash or an error message.
44
+ * @returns {Promise<TxHash>} A promise that resolves to the transaction hash.
45
45
  * @throws {"memo too long"} Thrown if the memo is longer than 80 characters.
46
46
  */
47
47
  transfer(params: TxParams & {
48
48
  feeRate?: FeeRate;
49
+ utxoSelectionPreferences?: UtxoSelectionPreferences;
50
+ selectedUtxos?: UTXO[];
49
51
  }): Promise<TxHash>;
52
+ /**
53
+ * Transfer the maximum amount of Bitcoin (sweep).
54
+ *
55
+ * Calculates the maximum sendable amount after fees, signs, and broadcasts the transaction.
56
+ * @param {Object} params The transfer parameters.
57
+ * @param {string} params.recipient The recipient address.
58
+ * @param {string} [params.memo] Optional memo for the transaction.
59
+ * @param {FeeRate} [params.feeRate] Optional fee rate. Defaults to 'fast' rate.
60
+ * @param {number} [params.walletIndex] Optional wallet index. Defaults to 0.
61
+ * @param {UtxoSelectionPreferences} [params.utxoSelectionPreferences] Optional UTXO selection preferences.
62
+ * @returns {Promise<{ hash: TxHash; maxAmount: number; fee: number }>} The transaction hash, amount sent, and fee.
63
+ */
64
+ transferMax(params: {
65
+ recipient: Address;
66
+ memo?: string;
67
+ feeRate?: FeeRate;
68
+ walletIndex?: number;
69
+ utxoSelectionPreferences?: UtxoSelectionPreferences;
70
+ selectedUtxos?: UTXO[];
71
+ }): Promise<{
72
+ hash: TxHash;
73
+ maxAmount: number;
74
+ fee: number;
75
+ }>;
50
76
  }
51
77
  export { ClientKeystore };
@@ -1,7 +1,7 @@
1
1
  import AppBtc from '@ledgerhq/hw-app-btc';
2
2
  import { FeeRate, TxHash } from '@xchainjs/xchain-client';
3
3
  import { Address } from '@xchainjs/xchain-util';
4
- import { TxParams, UtxoClientParams } from '@xchainjs/xchain-utxo';
4
+ import { TxParams, UTXO, UtxoClientParams, UtxoSelectionPreferences } from '@xchainjs/xchain-utxo';
5
5
  import { Client } from './client';
6
6
  import { AddressFormat } from './types';
7
7
  /**
@@ -19,6 +19,19 @@ declare class ClientLedger extends Client {
19
19
  getAddressAsync(index?: number, verify?: boolean): Promise<Address>;
20
20
  transfer(params: TxParams & {
21
21
  feeRate?: FeeRate;
22
+ utxoSelectionPreferences?: UtxoSelectionPreferences;
22
23
  }): Promise<TxHash>;
24
+ transferMax(params: {
25
+ recipient: Address;
26
+ memo?: string;
27
+ feeRate?: FeeRate;
28
+ walletIndex?: number;
29
+ utxoSelectionPreferences?: UtxoSelectionPreferences;
30
+ selectedUtxos?: UTXO[];
31
+ }): Promise<{
32
+ hash: TxHash;
33
+ maxAmount: number;
34
+ fee: number;
35
+ }>;
23
36
  }
24
37
  export { ClientLedger };
package/lib/index.esm.js CHANGED
@@ -4,8 +4,7 @@ import { getSeed } from '@xchainjs/xchain-crypto';
4
4
  import { HDKey } from '@scure/bip32';
5
5
  import * as Bitcoin from 'bitcoinjs-lib';
6
6
  import { ECPairFactory } from 'ecpair';
7
- import { Client as Client$1 } from '@xchainjs/xchain-utxo';
8
- import accumulative from 'coinselect/accumulative.js';
7
+ import { Client as Client$1, UtxoTransactionValidator, UtxoError } from '@xchainjs/xchain-utxo';
9
8
  import { AssetType } from '@xchainjs/xchain-util';
10
9
  import { SochainProvider, SochainNetwork, HaskoinProvider, HaskoinNetwork, BlockcypherProvider, BlockcypherNetwork, BitgoProvider } from '@xchainjs/xchain-utxo-providers';
11
10
  import AppBtc from '@ledgerhq/hw-app-btc';
@@ -160,7 +159,7 @@ const validateAddress = (address, network) => {
160
159
  Bitcoin.address.toOutputScript(address, btcNetwork(network)); // Try to convert the address to an output script using the specified network
161
160
  return true; // If successful, the address is valid
162
161
  }
163
- catch (_error) {
162
+ catch (_a) {
164
163
  return false; // If an error occurs, the address is invalid
165
164
  }
166
165
  };
@@ -285,98 +284,263 @@ class Client extends Client$1 {
285
284
  return fee > MIN_TX_FEE ? fee : MIN_TX_FEE;
286
285
  }
287
286
  /**
288
- * Build a Bitcoin transaction.*
287
+ * Enhanced Bitcoin transaction builder with comprehensive validation and optimal UTXO selection
288
+ * @param params Transaction parameters
289
+ * @returns Enhanced transaction build result with PSBT, UTXOs, and inputs
290
+ */
291
+ buildTxEnhanced(_a) {
292
+ return __awaiter(this, arguments, void 0, function* ({ amount, recipient, memo, feeRate, sender, spendPendingUTXO = true, utxoSelectionPreferences, selectedUtxos, }) {
293
+ try {
294
+ // Comprehensive input validation
295
+ this.validateTransactionInputs({
296
+ amount,
297
+ recipient,
298
+ memo,
299
+ sender,
300
+ feeRate,
301
+ });
302
+ // Use provided UTXOs (coin control) or fetch from chain
303
+ let utxos;
304
+ if (selectedUtxos && selectedUtxos.length > 0) {
305
+ UtxoTransactionValidator.validateUtxoSet(selectedUtxos);
306
+ utxos = selectedUtxos;
307
+ }
308
+ else {
309
+ const confirmedOnly = !spendPendingUTXO;
310
+ utxos = yield this.getValidatedUtxos(sender, confirmedOnly);
311
+ }
312
+ const compiledMemo = memo ? this.compileMemo(memo) : null;
313
+ const targetValue = amount.amount().toNumber();
314
+ const extraOutputs = 1 + (compiledMemo ? 1 : 0); // recipient + optional memo (change calculated separately)
315
+ // Enhanced UTXO selection
316
+ const selectionResult = this.selectUtxosForTransaction(utxos, targetValue, Math.ceil(feeRate), extraOutputs, utxoSelectionPreferences);
317
+ const psbt = new Bitcoin.Psbt({ network: btcNetwork(this.network) });
318
+ // Add inputs based on selection
319
+ if (this.addressFormat === AddressFormat.P2WPKH) {
320
+ selectionResult.inputs.forEach((utxo) => {
321
+ if (!utxo.witnessUtxo) {
322
+ throw UtxoError.fromUnknown(new Error(`Missing witnessUtxo for UTXO ${utxo.hash}:${utxo.index}`), 'buildTxPsbt');
323
+ }
324
+ psbt.addInput({
325
+ hash: utxo.hash,
326
+ index: utxo.index,
327
+ witnessUtxo: utxo.witnessUtxo,
328
+ });
329
+ });
330
+ }
331
+ else {
332
+ const { pubkey, output } = Bitcoin.payments.p2tr({
333
+ address: sender,
334
+ });
335
+ selectionResult.inputs.forEach((utxo) => psbt.addInput({
336
+ hash: utxo.hash,
337
+ index: utxo.index,
338
+ witnessUtxo: { value: utxo.value, script: output },
339
+ tapInternalKey: pubkey,
340
+ }));
341
+ }
342
+ // Add recipient output
343
+ psbt.addOutput({
344
+ address: recipient,
345
+ value: targetValue,
346
+ });
347
+ // Add change output if needed
348
+ if (selectionResult.changeAmount > 0) {
349
+ psbt.addOutput({
350
+ address: sender,
351
+ value: selectionResult.changeAmount,
352
+ });
353
+ }
354
+ // Add memo output if present
355
+ if (compiledMemo) {
356
+ psbt.addOutput({ script: compiledMemo, value: 0 });
357
+ }
358
+ return { psbt, utxos, inputs: selectionResult.inputs };
359
+ }
360
+ catch (error) {
361
+ if (UtxoError.isUtxoError(error)) {
362
+ throw error;
363
+ }
364
+ throw UtxoError.fromUnknown(error, 'buildTxEnhanced');
365
+ }
366
+ });
367
+ }
368
+ /**
369
+ * Build a Bitcoin transaction with enhanced validation and performance.
370
+ * Now uses the enhanced logic internally while maintaining the same API.
289
371
  * @param param0
290
- * @deprecated
291
372
  */
292
373
  buildTx(_a) {
293
374
  return __awaiter(this, arguments, void 0, function* ({ amount, recipient, memo, feeRate, sender, spendPendingUTXO = true, }) {
294
- // Check memo length
295
- if (memo && memo.length > 80) {
296
- throw new Error('memo too long, must not be longer than 80 chars.');
297
- }
298
- // This section of the code is responsible for preparing a transaction by building a Bitcoin PSBT (Partially Signed Bitcoin Transaction).
299
- if (!this.validateAddress(recipient))
300
- throw new Error('Invalid address');
301
- // Determine whether to only use confirmed UTXOs or include pending UTXOs based on the spendPendingUTXO flag.
302
- const confirmedOnly = !spendPendingUTXO;
303
- // Scan UTXOs associated with the sender's address.
304
- const utxos = yield this.scanUTXOs(sender, confirmedOnly);
305
- // Throw an error if there are no available UTXOs to cover the transaction.
306
- if (utxos.length === 0)
307
- throw new Error('Insufficient Balance for transaction');
308
- // Round up the fee rate to the nearest integer.
309
- const feeRateWhole = Math.ceil(feeRate);
310
- // Compile the memo into a Buffer if provided.
311
- const compiledMemo = memo ? this.compileMemo(memo) : null;
312
- // Initialize an array to store the target outputs of the transaction.
313
- const targetOutputs = [];
314
- // 1. Add the recipient address and amount to the target outputs.
315
- targetOutputs.push({
316
- address: recipient,
317
- value: amount.amount().toNumber(),
375
+ // Use the enhanced logic internally while maintaining the same API
376
+ return this.buildTxEnhanced({
377
+ amount,
378
+ recipient,
379
+ memo,
380
+ feeRate,
381
+ sender,
382
+ spendPendingUTXO,
318
383
  });
319
- // 2. Add the compiled memo to the target outputs if it exists.
320
- if (compiledMemo) {
321
- targetOutputs.push({ script: compiledMemo, value: 0 });
384
+ });
385
+ }
386
+ /**
387
+ * Send maximum possible amount (sweep) with optimal fee calculation
388
+ * @param params Send max parameters
389
+ * @returns Transaction details with maximum sendable amount
390
+ */
391
+ sendMax(_a) {
392
+ return __awaiter(this, arguments, void 0, function* ({ sender, recipient, memo, feeRate, spendPendingUTXO = true, utxoSelectionPreferences, selectedUtxos, }) {
393
+ try {
394
+ // Basic validation (skip amount validation since we're calculating max)
395
+ if (!(recipient === null || recipient === void 0 ? void 0 : recipient.trim())) {
396
+ throw UtxoError.invalidAddress(recipient, this.network);
397
+ }
398
+ if (!this.validateAddress(recipient)) {
399
+ throw UtxoError.invalidAddress(recipient, this.network);
400
+ }
401
+ if (!this.validateAddress(sender)) {
402
+ throw UtxoError.invalidAddress(sender, this.network);
403
+ }
404
+ // Memo validation is handled by validateTransactionInputs
405
+ // Use provided UTXOs (coin control) or fetch from chain
406
+ let utxos;
407
+ if (selectedUtxos && selectedUtxos.length > 0) {
408
+ UtxoTransactionValidator.validateUtxoSet(selectedUtxos);
409
+ utxos = selectedUtxos;
410
+ }
411
+ else {
412
+ const confirmedOnly = !spendPendingUTXO;
413
+ utxos = yield this.getValidatedUtxos(sender, confirmedOnly);
414
+ }
415
+ // Calculate maximum sendable amount
416
+ const maxCalc = this.calculateMaxSendableAmount(utxos, Math.ceil(feeRate), !!memo, utxoSelectionPreferences);
417
+ const compiledMemo = memo ? this.compileMemo(memo) : null;
418
+ const psbt = new Bitcoin.Psbt({ network: btcNetwork(this.network) });
419
+ // Add inputs
420
+ if (this.addressFormat === AddressFormat.P2WPKH) {
421
+ maxCalc.inputs.forEach((utxo) => {
422
+ if (!utxo.witnessUtxo) {
423
+ throw UtxoError.fromUnknown(new Error(`Missing witnessUtxo for UTXO ${utxo.hash}:${utxo.index}`), 'sendMax');
424
+ }
425
+ psbt.addInput({
426
+ hash: utxo.hash,
427
+ index: utxo.index,
428
+ witnessUtxo: utxo.witnessUtxo,
429
+ });
430
+ });
431
+ }
432
+ else {
433
+ const { pubkey, output } = Bitcoin.payments.p2tr({
434
+ address: sender,
435
+ });
436
+ maxCalc.inputs.forEach((utxo) => psbt.addInput({
437
+ hash: utxo.hash,
438
+ index: utxo.index,
439
+ witnessUtxo: { value: utxo.value, script: output },
440
+ tapInternalKey: pubkey,
441
+ }));
442
+ }
443
+ // Add recipient output (max amount - no change)
444
+ psbt.addOutput({
445
+ address: recipient,
446
+ value: maxCalc.amount,
447
+ });
448
+ // Add memo output if present
449
+ if (compiledMemo) {
450
+ psbt.addOutput({ script: compiledMemo, value: 0 });
451
+ }
452
+ return {
453
+ psbt,
454
+ utxos,
455
+ inputs: maxCalc.inputs,
456
+ maxAmount: maxCalc.amount,
457
+ fee: maxCalc.fee,
458
+ };
322
459
  }
323
- // Use the coinselect library to determine the inputs and outputs for the transaction.
324
- const { inputs, outputs } = accumulative(utxos, targetOutputs, feeRateWhole);
325
- // If no suitable inputs or outputs are found, throw an error indicating insufficient balance.
326
- if (!inputs || !outputs)
327
- throw new Error('Insufficient Balance for transaction');
328
- // Initialize a new Bitcoin PSBT object.
329
- const psbt = new Bitcoin.Psbt({ network: btcNetwork(this.network) }); // Network-specific
330
- if (this.addressFormat === AddressFormat.P2WPKH) {
331
- // Add inputs to the PSBT from the accumulated inputs.
332
- inputs.forEach((utxo) => psbt.addInput({
333
- hash: utxo.hash,
334
- index: utxo.index,
335
- witnessUtxo: utxo.witnessUtxo,
336
- }));
460
+ catch (error) {
461
+ if (UtxoError.isUtxoError(error)) {
462
+ throw error;
463
+ }
464
+ throw UtxoError.fromUnknown(error, 'sendMax');
337
465
  }
338
- else {
339
- const { pubkey, output } = Bitcoin.payments.p2tr({
340
- address: sender,
466
+ });
467
+ }
468
+ /**
469
+ * Prepare maximum amount transfer (sweep transaction)
470
+ * @param params Send max parameters
471
+ * @returns Prepared transaction with maximum sendable amount
472
+ */
473
+ prepareMaxTx(_a) {
474
+ return __awaiter(this, arguments, void 0, function* ({ sender, recipient, memo, feeRate, spendPendingUTXO = true, utxoSelectionPreferences, selectedUtxos, }) {
475
+ try {
476
+ const { psbt, utxos, inputs, maxAmount, fee } = yield this.sendMax({
477
+ sender,
478
+ recipient,
479
+ memo,
480
+ feeRate,
481
+ spendPendingUTXO,
482
+ utxoSelectionPreferences,
483
+ selectedUtxos,
341
484
  });
342
- inputs.forEach((utxo) => psbt.addInput({
343
- hash: utxo.hash,
344
- index: utxo.index,
345
- witnessUtxo: { value: utxo.value, script: output },
346
- tapInternalKey: pubkey,
347
- }));
485
+ return {
486
+ rawUnsignedTx: psbt.toBase64(),
487
+ utxos,
488
+ inputs,
489
+ maxAmount,
490
+ fee,
491
+ };
348
492
  }
349
- // Add outputs to the PSBT from the accumulated outputs.
350
- outputs.forEach((output) => {
351
- // If the output address is not specified, it's considered a change address and set to the sender's address.
352
- if (!output.address) {
353
- //an empty address means this is the change address
354
- output.address = sender;
493
+ catch (error) {
494
+ if (UtxoError.isUtxoError(error)) {
495
+ console.error('Bitcoin max transaction preparation failed:', error.toJSON());
496
+ throw error;
355
497
  }
356
- // Add the output to the PSBT.
357
- if (!output.script) {
358
- psbt.addOutput(output);
359
- }
360
- else {
361
- // If the output is a memo, add it to the PSBT to avoid dust error.
362
- if (compiledMemo) {
363
- psbt.addOutput({ script: compiledMemo, value: 0 });
364
- }
498
+ throw UtxoError.fromUnknown(error, 'prepareMaxTx');
499
+ }
500
+ });
501
+ }
502
+ /**
503
+ * Enhanced prepare transfer with comprehensive validation and optimal UTXO selection.
504
+ *
505
+ * @param params The transfer options with enhanced UTXO selection preferences.
506
+ * @returns The raw unsigned transaction with enhanced error handling.
507
+ */
508
+ prepareTxEnhanced(_a) {
509
+ return __awaiter(this, arguments, void 0, function* ({ sender, memo, amount, recipient, spendPendingUTXO = true, feeRate, utxoSelectionPreferences, selectedUtxos, }) {
510
+ try {
511
+ const { psbt, utxos, inputs } = yield this.buildTxEnhanced({
512
+ sender,
513
+ recipient,
514
+ amount,
515
+ feeRate,
516
+ memo,
517
+ spendPendingUTXO,
518
+ utxoSelectionPreferences,
519
+ selectedUtxos,
520
+ });
521
+ return { rawUnsignedTx: psbt.toBase64(), utxos, inputs };
522
+ }
523
+ catch (error) {
524
+ if (UtxoError.isUtxoError(error)) {
525
+ console.error('Enhanced Bitcoin transaction preparation failed:', error.toJSON());
526
+ throw error;
365
527
  }
366
- });
367
- return { psbt, utxos, inputs };
528
+ throw UtxoError.fromUnknown(error, 'prepareTxEnhanced');
529
+ }
368
530
  });
369
531
  }
370
532
  /**
371
- * Prepare transfer.
533
+ * Prepare transfer with enhanced validation and performance.
534
+ * Now uses the enhanced logic internally while maintaining the same API.
372
535
  *
536
+ * @deprecated Use `prepareTxEnhanced` directly for explicit enhanced UTXO selection.
373
537
  * @param {TxParams&Address&FeeRate&boolean} params The transfer options.
374
538
  * @returns {PreparedTx} The raw unsigned transaction.
375
539
  */
376
540
  prepareTx(_a) {
377
541
  return __awaiter(this, arguments, void 0, function* ({ sender, memo, amount, recipient, spendPendingUTXO = true, feeRate, }) {
378
- // Build the transaction using the provided parameters.
379
- const { psbt, utxos, inputs } = yield this.buildTx({
542
+ // Use the enhanced logic internally while maintaining the same API
543
+ return this.prepareTxEnhanced({
380
544
  sender,
381
545
  recipient,
382
546
  amount,
@@ -384,8 +548,6 @@ class Client extends Client$1 {
384
548
  memo,
385
549
  spendPendingUTXO,
386
550
  });
387
- // Return the raw unsigned transaction (PSBT) and associated UTXOs.
388
- return { rawUnsignedTx: psbt.toBase64(), utxos, inputs };
389
551
  });
390
552
  }
391
553
  }
@@ -471,7 +633,7 @@ class ClientKeystore extends Client {
471
633
  * Transfer BTC.
472
634
  *
473
635
  * @param {TxParams&FeeRate} params The transfer options including the fee rate.
474
- * @returns {Promise<TxHash|string>} A promise that resolves to the transaction hash or an error message.
636
+ * @returns {Promise<TxHash>} A promise that resolves to the transaction hash.
475
637
  * @throws {"memo too long"} Thrown if the memo is longer than 80 characters.
476
638
  */
477
639
  transfer(params) {
@@ -484,8 +646,10 @@ class ClientKeystore extends Client {
484
646
  const fromAddressIndex = (params === null || params === void 0 ? void 0 : params.walletIndex) || 0;
485
647
  // Get the Bitcoin keys
486
648
  const btcKeys = this.getBtcKeys(this.phrase, fromAddressIndex);
649
+ // Merge default preferences with caller-provided preferences
650
+ const mergedUtxoSelectionPreferences = Object.assign({ minimizeFee: true, avoidDust: true, minimizeInputs: false }, params.utxoSelectionPreferences);
487
651
  // Prepare the transaction
488
- const { rawUnsignedTx } = yield this.prepareTx(Object.assign(Object.assign({}, params), { sender: this.getAddress(fromAddressIndex), feeRate }));
652
+ const { rawUnsignedTx } = yield this.prepareTxEnhanced(Object.assign(Object.assign({}, params), { sender: this.getAddress(fromAddressIndex), feeRate, utxoSelectionPreferences: mergedUtxoSelectionPreferences, selectedUtxos: params.selectedUtxos }));
489
653
  // Build the PSBT
490
654
  const psbt = Bitcoin.Psbt.fromBase64(rawUnsignedTx);
491
655
  // Sign all inputs
@@ -496,18 +660,44 @@ class ClientKeystore extends Client {
496
660
  psbt.finalizeAllInputs();
497
661
  // Extract the transaction hex
498
662
  const txHex = psbt.extractTransaction().toHex();
499
- // Extract the transaction hash
500
- const txHash = psbt.extractTransaction().getId();
501
- try {
502
- // Broadcast the transaction and return the transaction hash
503
- const txId = yield this.roundRobinBroadcastTx(txHex);
504
- return txId;
505
- }
506
- catch (_err) {
507
- // If broadcasting fails, return an error message with a link to the explorer
508
- const error = `Server error, please check explorer for tx confirmation ${this.explorerProviders[this.network].getExplorerTxUrl(txHash)}`;
509
- return error;
510
- }
663
+ // Broadcast the transaction and return the transaction hash
664
+ return yield this.roundRobinBroadcastTx(txHex);
665
+ });
666
+ }
667
+ /**
668
+ * Transfer the maximum amount of Bitcoin (sweep).
669
+ *
670
+ * Calculates the maximum sendable amount after fees, signs, and broadcasts the transaction.
671
+ * @param {Object} params The transfer parameters.
672
+ * @param {string} params.recipient The recipient address.
673
+ * @param {string} [params.memo] Optional memo for the transaction.
674
+ * @param {FeeRate} [params.feeRate] Optional fee rate. Defaults to 'fast' rate.
675
+ * @param {number} [params.walletIndex] Optional wallet index. Defaults to 0.
676
+ * @param {UtxoSelectionPreferences} [params.utxoSelectionPreferences] Optional UTXO selection preferences.
677
+ * @returns {Promise<{ hash: TxHash; maxAmount: number; fee: number }>} The transaction hash, amount sent, and fee.
678
+ */
679
+ transferMax(params) {
680
+ return __awaiter(this, void 0, void 0, function* () {
681
+ const feeRate = params.feeRate || (yield this.getFeeRates())[FeeOption.Fast];
682
+ checkFeeBounds(this.feeBounds, feeRate);
683
+ const fromAddressIndex = params.walletIndex || 0;
684
+ const sender = yield this.getAddressAsync(fromAddressIndex);
685
+ const { psbt, maxAmount, fee } = yield this.sendMax({
686
+ sender,
687
+ recipient: params.recipient,
688
+ memo: params.memo,
689
+ feeRate,
690
+ utxoSelectionPreferences: params.utxoSelectionPreferences,
691
+ selectedUtxos: params.selectedUtxos,
692
+ });
693
+ const btcKeys = this.getBtcKeys(this.phrase, fromAddressIndex);
694
+ psbt.signAllInputs(this.addressFormat === AddressFormat.P2WPKH
695
+ ? btcKeys
696
+ : btcKeys.tweak(Bitcoin.crypto.taggedHash('TapTweak', toXOnly(btcKeys.publicKey))));
697
+ psbt.finalizeAllInputs();
698
+ const txHex = psbt.extractTransaction().toHex();
699
+ const hash = yield this.roundRobinBroadcastTx(txHex);
700
+ return { hash, maxAmount, fee };
511
701
  });
512
702
  }
513
703
  }
@@ -558,8 +748,16 @@ class ClientLedger extends Client {
558
748
  checkFeeBounds(this.feeBounds, feeRate);
559
749
  // Get sender address
560
750
  const sender = yield this.getAddressAsync(fromAddressIndex);
561
- // Prepare transaction
562
- const { rawUnsignedTx, inputs } = yield this.prepareTx(Object.assign(Object.assign({}, params), { sender, feeRate }));
751
+ // Create defaults and merge with caller-provided preferences
752
+ const defaults = {
753
+ minimizeFee: true,
754
+ avoidDust: true,
755
+ minimizeInputs: false,
756
+ };
757
+ const mergedUtxoSelectionPreferences = Object.assign(Object.assign({}, defaults), params.utxoSelectionPreferences);
758
+ // Prepare transaction using enhanced method with optimal UTXO selection
759
+ const { rawUnsignedTx, inputs } = yield this.prepareTxEnhanced(Object.assign(Object.assign({}, params), { sender,
760
+ feeRate, utxoSelectionPreferences: mergedUtxoSelectionPreferences }));
563
761
  const psbt = Bitcoin.Psbt.fromBase64(rawUnsignedTx);
564
762
  // Prepare Ledger inputs
565
763
  const ledgerInputs = inputs.map(({ txHex, hash, index }) => {
@@ -594,6 +792,50 @@ class ClientLedger extends Client {
594
792
  return txHash;
595
793
  });
596
794
  }
795
+ // Transfer max BTC from Ledger (sweep transaction)
796
+ transferMax(params) {
797
+ return __awaiter(this, void 0, void 0, function* () {
798
+ const app = yield this.getApp();
799
+ const fromAddressIndex = (params === null || params === void 0 ? void 0 : params.walletIndex) || 0;
800
+ const feeRate = params.feeRate || (yield this.getFeeRates())[FeeOption.Fast];
801
+ checkFeeBounds(this.feeBounds, feeRate);
802
+ const sender = yield this.getAddressAsync(fromAddressIndex);
803
+ const { rawUnsignedTx, inputs, maxAmount, fee } = yield this.prepareMaxTx({
804
+ sender,
805
+ recipient: params.recipient,
806
+ memo: params.memo,
807
+ feeRate,
808
+ utxoSelectionPreferences: params.utxoSelectionPreferences,
809
+ selectedUtxos: params.selectedUtxos,
810
+ });
811
+ const psbt = Bitcoin.Psbt.fromBase64(rawUnsignedTx);
812
+ const ledgerInputs = inputs.map(({ txHex, hash, index }) => {
813
+ if (!txHex) {
814
+ throw Error(`Missing 'txHex' for UTXO (txHash ${hash})`);
815
+ }
816
+ const utxoTx = Bitcoin.Transaction.fromHex(txHex);
817
+ const splittedTx = app.splitTransaction(txHex, utxoTx.hasWitnesses());
818
+ return [splittedTx, index, null, null];
819
+ });
820
+ const associatedKeysets = ledgerInputs.map(() => this.getFullDerivationPath(fromAddressIndex));
821
+ const unsignedHex = psbt.data.globalMap.unsignedTx.toBuffer().toString('hex');
822
+ const newTx = app.splitTransaction(unsignedHex, true);
823
+ const outputScriptHex = app.serializeTransactionOutputs(newTx).toString('hex');
824
+ const txHex = yield app.createPaymentTransaction({
825
+ inputs: ledgerInputs,
826
+ associatedKeysets,
827
+ outputScriptHex,
828
+ segwit: true,
829
+ useTrustedInputForSegwit: true,
830
+ additionals: [this.addressFormat === AddressFormat.P2TR ? 'bech32m' : 'bech32'],
831
+ });
832
+ const hash = yield this.broadcastTx(txHex);
833
+ if (!hash) {
834
+ throw Error('No Tx hash');
835
+ }
836
+ return { hash, maxAmount, fee };
837
+ });
838
+ }
597
839
  }
598
840
 
599
841
  export { AddressFormat, AssetBTC, BTCChain, BTC_DECIMAL, BTC_SATOSHI_SYMBOL, BTC_SYMBOL, BitgoProviders, BlockcypherDataProviders, ClientKeystore as Client, ClientLedger, HaskoinDataProviders, LOWER_FEE_BOUND, MIN_TX_FEE, SochainDataProviders, UPPER_FEE_BOUND, blockstreamExplorerProviders, defaultBTCParams, getPrefix, tapRootDerivationPaths, validateAddress };
package/lib/index.js CHANGED
@@ -7,7 +7,6 @@ var bip32 = require('@scure/bip32');
7
7
  var Bitcoin = require('bitcoinjs-lib');
8
8
  var ecpair = require('ecpair');
9
9
  var xchainUtxo = require('@xchainjs/xchain-utxo');
10
- var accumulative = require('coinselect/accumulative.js');
11
10
  var xchainUtil = require('@xchainjs/xchain-util');
12
11
  var xchainUtxoProviders = require('@xchainjs/xchain-utxo-providers');
13
12
  var AppBtc = require('@ledgerhq/hw-app-btc');
@@ -34,7 +33,6 @@ function _interopNamespace(e) {
34
33
 
35
34
  var ecc__namespace = /*#__PURE__*/_interopNamespace(ecc);
36
35
  var Bitcoin__namespace = /*#__PURE__*/_interopNamespace(Bitcoin);
37
- var accumulative__default = /*#__PURE__*/_interopDefault(accumulative);
38
36
  var AppBtc__default = /*#__PURE__*/_interopDefault(AppBtc);
39
37
 
40
38
  exports.AddressFormat = void 0;
@@ -187,7 +185,7 @@ const validateAddress = (address, network) => {
187
185
  Bitcoin__namespace.address.toOutputScript(address, btcNetwork(network)); // Try to convert the address to an output script using the specified network
188
186
  return true; // If successful, the address is valid
189
187
  }
190
- catch (_error) {
188
+ catch (_a) {
191
189
  return false; // If an error occurs, the address is invalid
192
190
  }
193
191
  };
@@ -312,98 +310,263 @@ class Client extends xchainUtxo.Client {
312
310
  return fee > MIN_TX_FEE ? fee : MIN_TX_FEE;
313
311
  }
314
312
  /**
315
- * Build a Bitcoin transaction.*
313
+ * Enhanced Bitcoin transaction builder with comprehensive validation and optimal UTXO selection
314
+ * @param params Transaction parameters
315
+ * @returns Enhanced transaction build result with PSBT, UTXOs, and inputs
316
+ */
317
+ buildTxEnhanced(_a) {
318
+ return __awaiter(this, arguments, void 0, function* ({ amount, recipient, memo, feeRate, sender, spendPendingUTXO = true, utxoSelectionPreferences, selectedUtxos, }) {
319
+ try {
320
+ // Comprehensive input validation
321
+ this.validateTransactionInputs({
322
+ amount,
323
+ recipient,
324
+ memo,
325
+ sender,
326
+ feeRate,
327
+ });
328
+ // Use provided UTXOs (coin control) or fetch from chain
329
+ let utxos;
330
+ if (selectedUtxos && selectedUtxos.length > 0) {
331
+ xchainUtxo.UtxoTransactionValidator.validateUtxoSet(selectedUtxos);
332
+ utxos = selectedUtxos;
333
+ }
334
+ else {
335
+ const confirmedOnly = !spendPendingUTXO;
336
+ utxos = yield this.getValidatedUtxos(sender, confirmedOnly);
337
+ }
338
+ const compiledMemo = memo ? this.compileMemo(memo) : null;
339
+ const targetValue = amount.amount().toNumber();
340
+ const extraOutputs = 1 + (compiledMemo ? 1 : 0); // recipient + optional memo (change calculated separately)
341
+ // Enhanced UTXO selection
342
+ const selectionResult = this.selectUtxosForTransaction(utxos, targetValue, Math.ceil(feeRate), extraOutputs, utxoSelectionPreferences);
343
+ const psbt = new Bitcoin__namespace.Psbt({ network: btcNetwork(this.network) });
344
+ // Add inputs based on selection
345
+ if (this.addressFormat === exports.AddressFormat.P2WPKH) {
346
+ selectionResult.inputs.forEach((utxo) => {
347
+ if (!utxo.witnessUtxo) {
348
+ throw xchainUtxo.UtxoError.fromUnknown(new Error(`Missing witnessUtxo for UTXO ${utxo.hash}:${utxo.index}`), 'buildTxPsbt');
349
+ }
350
+ psbt.addInput({
351
+ hash: utxo.hash,
352
+ index: utxo.index,
353
+ witnessUtxo: utxo.witnessUtxo,
354
+ });
355
+ });
356
+ }
357
+ else {
358
+ const { pubkey, output } = Bitcoin__namespace.payments.p2tr({
359
+ address: sender,
360
+ });
361
+ selectionResult.inputs.forEach((utxo) => psbt.addInput({
362
+ hash: utxo.hash,
363
+ index: utxo.index,
364
+ witnessUtxo: { value: utxo.value, script: output },
365
+ tapInternalKey: pubkey,
366
+ }));
367
+ }
368
+ // Add recipient output
369
+ psbt.addOutput({
370
+ address: recipient,
371
+ value: targetValue,
372
+ });
373
+ // Add change output if needed
374
+ if (selectionResult.changeAmount > 0) {
375
+ psbt.addOutput({
376
+ address: sender,
377
+ value: selectionResult.changeAmount,
378
+ });
379
+ }
380
+ // Add memo output if present
381
+ if (compiledMemo) {
382
+ psbt.addOutput({ script: compiledMemo, value: 0 });
383
+ }
384
+ return { psbt, utxos, inputs: selectionResult.inputs };
385
+ }
386
+ catch (error) {
387
+ if (xchainUtxo.UtxoError.isUtxoError(error)) {
388
+ throw error;
389
+ }
390
+ throw xchainUtxo.UtxoError.fromUnknown(error, 'buildTxEnhanced');
391
+ }
392
+ });
393
+ }
394
+ /**
395
+ * Build a Bitcoin transaction with enhanced validation and performance.
396
+ * Now uses the enhanced logic internally while maintaining the same API.
316
397
  * @param param0
317
- * @deprecated
318
398
  */
319
399
  buildTx(_a) {
320
400
  return __awaiter(this, arguments, void 0, function* ({ amount, recipient, memo, feeRate, sender, spendPendingUTXO = true, }) {
321
- // Check memo length
322
- if (memo && memo.length > 80) {
323
- throw new Error('memo too long, must not be longer than 80 chars.');
324
- }
325
- // This section of the code is responsible for preparing a transaction by building a Bitcoin PSBT (Partially Signed Bitcoin Transaction).
326
- if (!this.validateAddress(recipient))
327
- throw new Error('Invalid address');
328
- // Determine whether to only use confirmed UTXOs or include pending UTXOs based on the spendPendingUTXO flag.
329
- const confirmedOnly = !spendPendingUTXO;
330
- // Scan UTXOs associated with the sender's address.
331
- const utxos = yield this.scanUTXOs(sender, confirmedOnly);
332
- // Throw an error if there are no available UTXOs to cover the transaction.
333
- if (utxos.length === 0)
334
- throw new Error('Insufficient Balance for transaction');
335
- // Round up the fee rate to the nearest integer.
336
- const feeRateWhole = Math.ceil(feeRate);
337
- // Compile the memo into a Buffer if provided.
338
- const compiledMemo = memo ? this.compileMemo(memo) : null;
339
- // Initialize an array to store the target outputs of the transaction.
340
- const targetOutputs = [];
341
- // 1. Add the recipient address and amount to the target outputs.
342
- targetOutputs.push({
343
- address: recipient,
344
- value: amount.amount().toNumber(),
401
+ // Use the enhanced logic internally while maintaining the same API
402
+ return this.buildTxEnhanced({
403
+ amount,
404
+ recipient,
405
+ memo,
406
+ feeRate,
407
+ sender,
408
+ spendPendingUTXO,
345
409
  });
346
- // 2. Add the compiled memo to the target outputs if it exists.
347
- if (compiledMemo) {
348
- targetOutputs.push({ script: compiledMemo, value: 0 });
410
+ });
411
+ }
412
+ /**
413
+ * Send maximum possible amount (sweep) with optimal fee calculation
414
+ * @param params Send max parameters
415
+ * @returns Transaction details with maximum sendable amount
416
+ */
417
+ sendMax(_a) {
418
+ return __awaiter(this, arguments, void 0, function* ({ sender, recipient, memo, feeRate, spendPendingUTXO = true, utxoSelectionPreferences, selectedUtxos, }) {
419
+ try {
420
+ // Basic validation (skip amount validation since we're calculating max)
421
+ if (!(recipient === null || recipient === void 0 ? void 0 : recipient.trim())) {
422
+ throw xchainUtxo.UtxoError.invalidAddress(recipient, this.network);
423
+ }
424
+ if (!this.validateAddress(recipient)) {
425
+ throw xchainUtxo.UtxoError.invalidAddress(recipient, this.network);
426
+ }
427
+ if (!this.validateAddress(sender)) {
428
+ throw xchainUtxo.UtxoError.invalidAddress(sender, this.network);
429
+ }
430
+ // Memo validation is handled by validateTransactionInputs
431
+ // Use provided UTXOs (coin control) or fetch from chain
432
+ let utxos;
433
+ if (selectedUtxos && selectedUtxos.length > 0) {
434
+ xchainUtxo.UtxoTransactionValidator.validateUtxoSet(selectedUtxos);
435
+ utxos = selectedUtxos;
436
+ }
437
+ else {
438
+ const confirmedOnly = !spendPendingUTXO;
439
+ utxos = yield this.getValidatedUtxos(sender, confirmedOnly);
440
+ }
441
+ // Calculate maximum sendable amount
442
+ const maxCalc = this.calculateMaxSendableAmount(utxos, Math.ceil(feeRate), !!memo, utxoSelectionPreferences);
443
+ const compiledMemo = memo ? this.compileMemo(memo) : null;
444
+ const psbt = new Bitcoin__namespace.Psbt({ network: btcNetwork(this.network) });
445
+ // Add inputs
446
+ if (this.addressFormat === exports.AddressFormat.P2WPKH) {
447
+ maxCalc.inputs.forEach((utxo) => {
448
+ if (!utxo.witnessUtxo) {
449
+ throw xchainUtxo.UtxoError.fromUnknown(new Error(`Missing witnessUtxo for UTXO ${utxo.hash}:${utxo.index}`), 'sendMax');
450
+ }
451
+ psbt.addInput({
452
+ hash: utxo.hash,
453
+ index: utxo.index,
454
+ witnessUtxo: utxo.witnessUtxo,
455
+ });
456
+ });
457
+ }
458
+ else {
459
+ const { pubkey, output } = Bitcoin__namespace.payments.p2tr({
460
+ address: sender,
461
+ });
462
+ maxCalc.inputs.forEach((utxo) => psbt.addInput({
463
+ hash: utxo.hash,
464
+ index: utxo.index,
465
+ witnessUtxo: { value: utxo.value, script: output },
466
+ tapInternalKey: pubkey,
467
+ }));
468
+ }
469
+ // Add recipient output (max amount - no change)
470
+ psbt.addOutput({
471
+ address: recipient,
472
+ value: maxCalc.amount,
473
+ });
474
+ // Add memo output if present
475
+ if (compiledMemo) {
476
+ psbt.addOutput({ script: compiledMemo, value: 0 });
477
+ }
478
+ return {
479
+ psbt,
480
+ utxos,
481
+ inputs: maxCalc.inputs,
482
+ maxAmount: maxCalc.amount,
483
+ fee: maxCalc.fee,
484
+ };
349
485
  }
350
- // Use the coinselect library to determine the inputs and outputs for the transaction.
351
- const { inputs, outputs } = accumulative__default.default(utxos, targetOutputs, feeRateWhole);
352
- // If no suitable inputs or outputs are found, throw an error indicating insufficient balance.
353
- if (!inputs || !outputs)
354
- throw new Error('Insufficient Balance for transaction');
355
- // Initialize a new Bitcoin PSBT object.
356
- const psbt = new Bitcoin__namespace.Psbt({ network: btcNetwork(this.network) }); // Network-specific
357
- if (this.addressFormat === exports.AddressFormat.P2WPKH) {
358
- // Add inputs to the PSBT from the accumulated inputs.
359
- inputs.forEach((utxo) => psbt.addInput({
360
- hash: utxo.hash,
361
- index: utxo.index,
362
- witnessUtxo: utxo.witnessUtxo,
363
- }));
486
+ catch (error) {
487
+ if (xchainUtxo.UtxoError.isUtxoError(error)) {
488
+ throw error;
489
+ }
490
+ throw xchainUtxo.UtxoError.fromUnknown(error, 'sendMax');
364
491
  }
365
- else {
366
- const { pubkey, output } = Bitcoin__namespace.payments.p2tr({
367
- address: sender,
492
+ });
493
+ }
494
+ /**
495
+ * Prepare maximum amount transfer (sweep transaction)
496
+ * @param params Send max parameters
497
+ * @returns Prepared transaction with maximum sendable amount
498
+ */
499
+ prepareMaxTx(_a) {
500
+ return __awaiter(this, arguments, void 0, function* ({ sender, recipient, memo, feeRate, spendPendingUTXO = true, utxoSelectionPreferences, selectedUtxos, }) {
501
+ try {
502
+ const { psbt, utxos, inputs, maxAmount, fee } = yield this.sendMax({
503
+ sender,
504
+ recipient,
505
+ memo,
506
+ feeRate,
507
+ spendPendingUTXO,
508
+ utxoSelectionPreferences,
509
+ selectedUtxos,
368
510
  });
369
- inputs.forEach((utxo) => psbt.addInput({
370
- hash: utxo.hash,
371
- index: utxo.index,
372
- witnessUtxo: { value: utxo.value, script: output },
373
- tapInternalKey: pubkey,
374
- }));
511
+ return {
512
+ rawUnsignedTx: psbt.toBase64(),
513
+ utxos,
514
+ inputs,
515
+ maxAmount,
516
+ fee,
517
+ };
375
518
  }
376
- // Add outputs to the PSBT from the accumulated outputs.
377
- outputs.forEach((output) => {
378
- // If the output address is not specified, it's considered a change address and set to the sender's address.
379
- if (!output.address) {
380
- //an empty address means this is the change address
381
- output.address = sender;
519
+ catch (error) {
520
+ if (xchainUtxo.UtxoError.isUtxoError(error)) {
521
+ console.error('Bitcoin max transaction preparation failed:', error.toJSON());
522
+ throw error;
382
523
  }
383
- // Add the output to the PSBT.
384
- if (!output.script) {
385
- psbt.addOutput(output);
386
- }
387
- else {
388
- // If the output is a memo, add it to the PSBT to avoid dust error.
389
- if (compiledMemo) {
390
- psbt.addOutput({ script: compiledMemo, value: 0 });
391
- }
524
+ throw xchainUtxo.UtxoError.fromUnknown(error, 'prepareMaxTx');
525
+ }
526
+ });
527
+ }
528
+ /**
529
+ * Enhanced prepare transfer with comprehensive validation and optimal UTXO selection.
530
+ *
531
+ * @param params The transfer options with enhanced UTXO selection preferences.
532
+ * @returns The raw unsigned transaction with enhanced error handling.
533
+ */
534
+ prepareTxEnhanced(_a) {
535
+ return __awaiter(this, arguments, void 0, function* ({ sender, memo, amount, recipient, spendPendingUTXO = true, feeRate, utxoSelectionPreferences, selectedUtxos, }) {
536
+ try {
537
+ const { psbt, utxos, inputs } = yield this.buildTxEnhanced({
538
+ sender,
539
+ recipient,
540
+ amount,
541
+ feeRate,
542
+ memo,
543
+ spendPendingUTXO,
544
+ utxoSelectionPreferences,
545
+ selectedUtxos,
546
+ });
547
+ return { rawUnsignedTx: psbt.toBase64(), utxos, inputs };
548
+ }
549
+ catch (error) {
550
+ if (xchainUtxo.UtxoError.isUtxoError(error)) {
551
+ console.error('Enhanced Bitcoin transaction preparation failed:', error.toJSON());
552
+ throw error;
392
553
  }
393
- });
394
- return { psbt, utxos, inputs };
554
+ throw xchainUtxo.UtxoError.fromUnknown(error, 'prepareTxEnhanced');
555
+ }
395
556
  });
396
557
  }
397
558
  /**
398
- * Prepare transfer.
559
+ * Prepare transfer with enhanced validation and performance.
560
+ * Now uses the enhanced logic internally while maintaining the same API.
399
561
  *
562
+ * @deprecated Use `prepareTxEnhanced` directly for explicit enhanced UTXO selection.
400
563
  * @param {TxParams&Address&FeeRate&boolean} params The transfer options.
401
564
  * @returns {PreparedTx} The raw unsigned transaction.
402
565
  */
403
566
  prepareTx(_a) {
404
567
  return __awaiter(this, arguments, void 0, function* ({ sender, memo, amount, recipient, spendPendingUTXO = true, feeRate, }) {
405
- // Build the transaction using the provided parameters.
406
- const { psbt, utxos, inputs } = yield this.buildTx({
568
+ // Use the enhanced logic internally while maintaining the same API
569
+ return this.prepareTxEnhanced({
407
570
  sender,
408
571
  recipient,
409
572
  amount,
@@ -411,8 +574,6 @@ class Client extends xchainUtxo.Client {
411
574
  memo,
412
575
  spendPendingUTXO,
413
576
  });
414
- // Return the raw unsigned transaction (PSBT) and associated UTXOs.
415
- return { rawUnsignedTx: psbt.toBase64(), utxos, inputs };
416
577
  });
417
578
  }
418
579
  }
@@ -498,7 +659,7 @@ class ClientKeystore extends Client {
498
659
  * Transfer BTC.
499
660
  *
500
661
  * @param {TxParams&FeeRate} params The transfer options including the fee rate.
501
- * @returns {Promise<TxHash|string>} A promise that resolves to the transaction hash or an error message.
662
+ * @returns {Promise<TxHash>} A promise that resolves to the transaction hash.
502
663
  * @throws {"memo too long"} Thrown if the memo is longer than 80 characters.
503
664
  */
504
665
  transfer(params) {
@@ -511,8 +672,10 @@ class ClientKeystore extends Client {
511
672
  const fromAddressIndex = (params === null || params === void 0 ? void 0 : params.walletIndex) || 0;
512
673
  // Get the Bitcoin keys
513
674
  const btcKeys = this.getBtcKeys(this.phrase, fromAddressIndex);
675
+ // Merge default preferences with caller-provided preferences
676
+ const mergedUtxoSelectionPreferences = Object.assign({ minimizeFee: true, avoidDust: true, minimizeInputs: false }, params.utxoSelectionPreferences);
514
677
  // Prepare the transaction
515
- const { rawUnsignedTx } = yield this.prepareTx(Object.assign(Object.assign({}, params), { sender: this.getAddress(fromAddressIndex), feeRate }));
678
+ const { rawUnsignedTx } = yield this.prepareTxEnhanced(Object.assign(Object.assign({}, params), { sender: this.getAddress(fromAddressIndex), feeRate, utxoSelectionPreferences: mergedUtxoSelectionPreferences, selectedUtxos: params.selectedUtxos }));
516
679
  // Build the PSBT
517
680
  const psbt = Bitcoin__namespace.Psbt.fromBase64(rawUnsignedTx);
518
681
  // Sign all inputs
@@ -523,18 +686,44 @@ class ClientKeystore extends Client {
523
686
  psbt.finalizeAllInputs();
524
687
  // Extract the transaction hex
525
688
  const txHex = psbt.extractTransaction().toHex();
526
- // Extract the transaction hash
527
- const txHash = psbt.extractTransaction().getId();
528
- try {
529
- // Broadcast the transaction and return the transaction hash
530
- const txId = yield this.roundRobinBroadcastTx(txHex);
531
- return txId;
532
- }
533
- catch (_err) {
534
- // If broadcasting fails, return an error message with a link to the explorer
535
- const error = `Server error, please check explorer for tx confirmation ${this.explorerProviders[this.network].getExplorerTxUrl(txHash)}`;
536
- return error;
537
- }
689
+ // Broadcast the transaction and return the transaction hash
690
+ return yield this.roundRobinBroadcastTx(txHex);
691
+ });
692
+ }
693
+ /**
694
+ * Transfer the maximum amount of Bitcoin (sweep).
695
+ *
696
+ * Calculates the maximum sendable amount after fees, signs, and broadcasts the transaction.
697
+ * @param {Object} params The transfer parameters.
698
+ * @param {string} params.recipient The recipient address.
699
+ * @param {string} [params.memo] Optional memo for the transaction.
700
+ * @param {FeeRate} [params.feeRate] Optional fee rate. Defaults to 'fast' rate.
701
+ * @param {number} [params.walletIndex] Optional wallet index. Defaults to 0.
702
+ * @param {UtxoSelectionPreferences} [params.utxoSelectionPreferences] Optional UTXO selection preferences.
703
+ * @returns {Promise<{ hash: TxHash; maxAmount: number; fee: number }>} The transaction hash, amount sent, and fee.
704
+ */
705
+ transferMax(params) {
706
+ return __awaiter(this, void 0, void 0, function* () {
707
+ const feeRate = params.feeRate || (yield this.getFeeRates())[xchainClient.FeeOption.Fast];
708
+ xchainClient.checkFeeBounds(this.feeBounds, feeRate);
709
+ const fromAddressIndex = params.walletIndex || 0;
710
+ const sender = yield this.getAddressAsync(fromAddressIndex);
711
+ const { psbt, maxAmount, fee } = yield this.sendMax({
712
+ sender,
713
+ recipient: params.recipient,
714
+ memo: params.memo,
715
+ feeRate,
716
+ utxoSelectionPreferences: params.utxoSelectionPreferences,
717
+ selectedUtxos: params.selectedUtxos,
718
+ });
719
+ const btcKeys = this.getBtcKeys(this.phrase, fromAddressIndex);
720
+ psbt.signAllInputs(this.addressFormat === exports.AddressFormat.P2WPKH
721
+ ? btcKeys
722
+ : btcKeys.tweak(Bitcoin__namespace.crypto.taggedHash('TapTweak', toXOnly(btcKeys.publicKey))));
723
+ psbt.finalizeAllInputs();
724
+ const txHex = psbt.extractTransaction().toHex();
725
+ const hash = yield this.roundRobinBroadcastTx(txHex);
726
+ return { hash, maxAmount, fee };
538
727
  });
539
728
  }
540
729
  }
@@ -585,8 +774,16 @@ class ClientLedger extends Client {
585
774
  xchainClient.checkFeeBounds(this.feeBounds, feeRate);
586
775
  // Get sender address
587
776
  const sender = yield this.getAddressAsync(fromAddressIndex);
588
- // Prepare transaction
589
- const { rawUnsignedTx, inputs } = yield this.prepareTx(Object.assign(Object.assign({}, params), { sender, feeRate }));
777
+ // Create defaults and merge with caller-provided preferences
778
+ const defaults = {
779
+ minimizeFee: true,
780
+ avoidDust: true,
781
+ minimizeInputs: false,
782
+ };
783
+ const mergedUtxoSelectionPreferences = Object.assign(Object.assign({}, defaults), params.utxoSelectionPreferences);
784
+ // Prepare transaction using enhanced method with optimal UTXO selection
785
+ const { rawUnsignedTx, inputs } = yield this.prepareTxEnhanced(Object.assign(Object.assign({}, params), { sender,
786
+ feeRate, utxoSelectionPreferences: mergedUtxoSelectionPreferences }));
590
787
  const psbt = Bitcoin__namespace.Psbt.fromBase64(rawUnsignedTx);
591
788
  // Prepare Ledger inputs
592
789
  const ledgerInputs = inputs.map(({ txHex, hash, index }) => {
@@ -621,6 +818,50 @@ class ClientLedger extends Client {
621
818
  return txHash;
622
819
  });
623
820
  }
821
+ // Transfer max BTC from Ledger (sweep transaction)
822
+ transferMax(params) {
823
+ return __awaiter(this, void 0, void 0, function* () {
824
+ const app = yield this.getApp();
825
+ const fromAddressIndex = (params === null || params === void 0 ? void 0 : params.walletIndex) || 0;
826
+ const feeRate = params.feeRate || (yield this.getFeeRates())[xchainClient.FeeOption.Fast];
827
+ xchainClient.checkFeeBounds(this.feeBounds, feeRate);
828
+ const sender = yield this.getAddressAsync(fromAddressIndex);
829
+ const { rawUnsignedTx, inputs, maxAmount, fee } = yield this.prepareMaxTx({
830
+ sender,
831
+ recipient: params.recipient,
832
+ memo: params.memo,
833
+ feeRate,
834
+ utxoSelectionPreferences: params.utxoSelectionPreferences,
835
+ selectedUtxos: params.selectedUtxos,
836
+ });
837
+ const psbt = Bitcoin__namespace.Psbt.fromBase64(rawUnsignedTx);
838
+ const ledgerInputs = inputs.map(({ txHex, hash, index }) => {
839
+ if (!txHex) {
840
+ throw Error(`Missing 'txHex' for UTXO (txHash ${hash})`);
841
+ }
842
+ const utxoTx = Bitcoin__namespace.Transaction.fromHex(txHex);
843
+ const splittedTx = app.splitTransaction(txHex, utxoTx.hasWitnesses());
844
+ return [splittedTx, index, null, null];
845
+ });
846
+ const associatedKeysets = ledgerInputs.map(() => this.getFullDerivationPath(fromAddressIndex));
847
+ const unsignedHex = psbt.data.globalMap.unsignedTx.toBuffer().toString('hex');
848
+ const newTx = app.splitTransaction(unsignedHex, true);
849
+ const outputScriptHex = app.serializeTransactionOutputs(newTx).toString('hex');
850
+ const txHex = yield app.createPaymentTransaction({
851
+ inputs: ledgerInputs,
852
+ associatedKeysets,
853
+ outputScriptHex,
854
+ segwit: true,
855
+ useTrustedInputForSegwit: true,
856
+ additionals: [this.addressFormat === exports.AddressFormat.P2TR ? 'bech32m' : 'bech32'],
857
+ });
858
+ const hash = yield this.broadcastTx(txHex);
859
+ if (!hash) {
860
+ throw Error('No Tx hash');
861
+ }
862
+ return { hash, maxAmount, fee };
863
+ });
864
+ }
624
865
  }
625
866
 
626
867
  exports.AssetBTC = AssetBTC;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@xchainjs/xchain-bitcoin",
3
- "version": "2.0.10",
3
+ "version": "2.2.0",
4
4
  "description": "Custom Bitcoin client and utilities used by XChainJS clients",
5
5
  "keywords": [
6
6
  "XChain",
@@ -39,7 +39,7 @@
39
39
  "@xchainjs/xchain-client": "2.0.10",
40
40
  "@xchainjs/xchain-crypto": "1.0.6",
41
41
  "@xchainjs/xchain-util": "2.0.5",
42
- "@xchainjs/xchain-utxo": "2.0.10",
42
+ "@xchainjs/xchain-utxo": "2.2.0",
43
43
  "@xchainjs/xchain-utxo-providers": "2.0.10",
44
44
  "bitcoinjs-lib": "^6.1.7",
45
45
  "coinselect": "3.1.12",