opnet 1.8.2 → 1.8.3

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.
Files changed (47) hide show
  1. package/CHANGELOG.md +5 -0
  2. package/browser/_version.d.ts +1 -1
  3. package/browser/contracts/interfaces/IProviderForCallResult.d.ts +2 -0
  4. package/browser/index.js +174 -63
  5. package/browser/opnet.d.ts +1 -0
  6. package/browser/providers/AbstractRpcProvider.d.ts +2 -0
  7. package/browser/providers/interfaces/JSONRpcMethods.d.ts +1 -0
  8. package/browser/providers/websocket/types/WebSocketOpcodes.d.ts +2 -0
  9. package/browser/transactions/interfaces/BroadcastedTransactionPackage.d.ts +48 -0
  10. package/browser/utxos/UTXOsManager.d.ts +1 -0
  11. package/build/_version.d.ts +1 -1
  12. package/build/_version.js +1 -1
  13. package/build/contracts/CallResult.js +54 -19
  14. package/build/contracts/interfaces/IProviderForCallResult.d.ts +2 -0
  15. package/build/opnet.d.ts +1 -0
  16. package/build/opnet.js +1 -0
  17. package/build/providers/AbstractRpcProvider.d.ts +2 -0
  18. package/build/providers/AbstractRpcProvider.js +15 -2
  19. package/build/providers/WebsocketRpcProvider.js +5 -0
  20. package/build/providers/interfaces/JSONRpcMethods.d.ts +1 -0
  21. package/build/providers/interfaces/JSONRpcMethods.js +1 -0
  22. package/build/providers/websocket/MethodMapping.js +6 -0
  23. package/build/providers/websocket/types/WebSocketOpcodes.d.ts +2 -0
  24. package/build/providers/websocket/types/WebSocketOpcodes.js +2 -0
  25. package/build/transactions/interfaces/BroadcastedTransactionPackage.d.ts +48 -0
  26. package/build/transactions/interfaces/BroadcastedTransactionPackage.js +1 -0
  27. package/build/tsconfig.build.tsbuildinfo +1 -1
  28. package/build/utxos/UTXOsManager.d.ts +1 -0
  29. package/build/utxos/UTXOsManager.js +37 -28
  30. package/docs/api-reference/provider-api.md +16 -0
  31. package/docs/api-reference/types-interfaces.md +91 -0
  32. package/docs/api-reference/utxo-manager-api.md +4 -2
  33. package/docs/svg/tx-broadcast-flow.svg +201 -0
  34. package/docs/transactions/broadcasting.md +124 -9
  35. package/package.json +2 -3
  36. package/src/_version.ts +1 -1
  37. package/src/contracts/CallResult.ts +82 -32
  38. package/src/contracts/Contract.ts +11 -4
  39. package/src/contracts/interfaces/IProviderForCallResult.ts +5 -0
  40. package/src/opnet.ts +1 -0
  41. package/src/providers/AbstractRpcProvider.ts +52 -5
  42. package/src/providers/WebsocketRpcProvider.ts +7 -0
  43. package/src/providers/interfaces/JSONRpcMethods.ts +1 -0
  44. package/src/providers/websocket/MethodMapping.ts +6 -0
  45. package/src/providers/websocket/types/WebSocketOpcodes.ts +2 -0
  46. package/src/transactions/interfaces/BroadcastedTransactionPackage.ts +72 -0
  47. package/src/utxos/UTXOsManager.ts +82 -46
@@ -1,13 +1,5 @@
1
1
  import { QuantumBIP32Interface } from '@btc-vision/bip32';
2
- import {
3
- fromBase64,
4
- fromHex,
5
- Network,
6
- networks,
7
- PsbtOutputExtended,
8
- Signer,
9
- toHex,
10
- } from '@btc-vision/bitcoin';
2
+ import { fromBase64, fromHex, Network, networks, PsbtOutputExtended, Signer, toHex, } from '@btc-vision/bitcoin';
11
3
  import { UniversalSigner } from '@btc-vision/ecpair';
12
4
  import {
13
5
  Address,
@@ -24,6 +16,7 @@ import {
24
16
  } from '@btc-vision/transaction';
25
17
  import { UTXO } from '../bitcoin/UTXOs.js';
26
18
  import { BitcoinFees } from '../block/BlockGasParameters.js';
19
+ import { PackageResult } from '../transactions/interfaces/BroadcastedTransactionPackage.js';
27
20
  import { decodeRevertData } from '../utils/RevertDecoder.js';
28
21
  import { RequestUTXOsParamsWithAmount } from '../utxos/interfaces/IUTXOsManager.js';
29
22
  import { CallResultSerializer, NetworkName } from './CallResultSerializer.js';
@@ -111,6 +104,23 @@ export interface InteractionTransactionReceipt {
111
104
  readonly compiledTargetScript: string | null;
112
105
  }
113
106
 
107
+ function extractPackageFailures(packageResult: PackageResult): string[] {
108
+ const failures: string[] = [];
109
+ const results = packageResult.txResults;
110
+
111
+ for (const [submittedTxid, result] of Object.entries(results)) {
112
+ if (result.error) {
113
+ failures.push(`tx ${submittedTxid} failed: ${result.error}`);
114
+ }
115
+ }
116
+
117
+ if (failures.length === 0 && packageResult.packageMsg !== 'success') {
118
+ failures.push(`package rejected: ${packageResult.packageMsg}`);
119
+ }
120
+
121
+ return failures;
122
+ }
123
+
114
124
  /**
115
125
  * Represents the result of a contract call.
116
126
  * @category Contracts
@@ -244,6 +254,13 @@ export class CallResult<
244
254
  ),
245
255
  );
246
256
  },
257
+ sendRawTransactionPackage: () => {
258
+ return Promise.reject(
259
+ new Error(
260
+ 'Cannot broadcast from offline CallResult. Export signed transaction and broadcast online.',
261
+ ),
262
+ );
263
+ },
247
264
  getCSV1ForAddress: () => {
248
265
  if (!data.csvAddress) {
249
266
  throw new Error('CSV address not available in offline data');
@@ -461,54 +478,87 @@ export class CallResult<
461
478
 
462
479
  /**
463
480
  * Broadcasts a pre-signed interaction transaction.
481
+ * Uses sendRawTransactionPackage for atomic broadcast when a funding tx is present,
482
+ * falls back to sendRawTransaction for P2WDA (interaction-only) transactions.
464
483
  * @param {SignedInteractionTransactionReceipt} signedTx - The signed transaction data.
465
484
  * @returns {Promise<InteractionTransactionReceipt>} The transaction receipt with broadcast results.
466
485
  */
467
486
  public async sendPresignedTransaction(
468
487
  signedTx: SignedInteractionTransactionReceipt,
469
488
  ): Promise<InteractionTransactionReceipt> {
470
- if (!signedTx.utxoTracking.isP2WDA) {
471
- if (!signedTx.fundingTransactionRaw) {
472
- throw new Error('Funding transaction not created');
473
- }
474
-
475
- const tx1 = await this.#provider.sendRawTransaction(
476
- signedTx.fundingTransactionRaw,
489
+ if (signedTx.utxoTracking.isP2WDA || !signedTx.fundingTransactionRaw) {
490
+ // P2WDA or no funding tx — broadcast interaction tx alone
491
+ const tx = await this.#provider.sendRawTransaction(
492
+ signedTx.interactionTransactionRaw,
477
493
  false,
478
494
  );
479
495
 
480
- if (!tx1 || tx1.error) {
481
- throw new Error(`Error sending transaction: ${tx1?.error || 'Unknown error'}`);
496
+ if (!tx || tx.error) {
497
+ throw new Error(`Error sending transaction: ${tx?.error || 'Unknown error'}`);
482
498
  }
483
499
 
484
- if (!tx1.success) {
485
- throw new Error(`Error sending transaction: ${tx1.result || 'Unknown error'}`);
500
+ if (!tx.result) {
501
+ throw new Error('No transaction ID returned');
486
502
  }
487
- }
488
503
 
489
- const tx2 = await this.#provider.sendRawTransaction(
490
- signedTx.interactionTransactionRaw,
491
- false,
504
+ if (!tx.success) {
505
+ throw new Error(`Error sending transaction: ${tx.result || 'Unknown error'}`);
506
+ }
507
+
508
+ this.#processUTXOTracking(signedTx);
509
+
510
+ return {
511
+ interactionAddress: signedTx.interactionAddress,
512
+ transactionId: tx.result,
513
+ peerAcknowledgements: tx.peers || 0,
514
+ newUTXOs: signedTx.nextUTXOs,
515
+ estimatedFees: signedTx.estimatedFees,
516
+ challengeSolution: signedTx.challengeSolution,
517
+ rawTransaction: signedTx.interactionTransactionRaw,
518
+ fundingUTXOs: signedTx.fundingUTXOs,
519
+ fundingInputUtxos: signedTx.fundingInputUtxos,
520
+ compiledTargetScript: signedTx.compiledTargetScript,
521
+ };
522
+ }
523
+
524
+ // Package broadcast: [funding, interaction]
525
+ const result = await this.#provider.sendRawTransactionPackage(
526
+ [signedTx.fundingTransactionRaw, signedTx.interactionTransactionRaw],
527
+ true,
492
528
  );
493
529
 
494
- if (!tx2 || tx2.error) {
495
- throw new Error(`Error sending transaction: ${tx2?.error || 'Unknown error'}`);
530
+ if (!result.success) {
531
+ throw new Error(
532
+ `Error sending transaction package: ${result.error || 'Unknown error'}`,
533
+ );
496
534
  }
497
535
 
498
- if (!tx2.result) {
499
- throw new Error('No transaction ID returned');
536
+ // Check submitPackage per-tx failures if packageResult is present
537
+ if (result.packageResult) {
538
+ const failures = extractPackageFailures(result.packageResult);
539
+ if (failures.length > 0) {
540
+ throw new Error(`Transaction package failed:\n${failures.join('\n')}`);
541
+ }
500
542
  }
501
543
 
502
- if (!tx2.success) {
503
- throw new Error(`Error sending transaction: ${tx2.result || 'Unknown error'}`);
544
+ // Extract the interaction tx result (second tx in the package)
545
+ const interactionSeqResult = result.sequentialResults?.[1];
546
+ if (interactionSeqResult && !interactionSeqResult.success) {
547
+ throw new Error(
548
+ `Interaction transaction failed: ${interactionSeqResult.error || 'Unknown error'}`,
549
+ );
504
550
  }
505
551
 
552
+ const interactionTxId = interactionSeqResult?.txid || signedTx.interactionTransactionRaw;
553
+
554
+ const peers = interactionSeqResult?.peers || 0;
555
+
506
556
  this.#processUTXOTracking(signedTx);
507
557
 
508
558
  return {
509
559
  interactionAddress: signedTx.interactionAddress,
510
- transactionId: tx2.result,
511
- peerAcknowledgements: tx2.peers || 0,
560
+ transactionId: interactionTxId,
561
+ peerAcknowledgements: peers,
512
562
  newUTXOs: signedTx.nextUTXOs,
513
563
  estimatedFees: signedTx.estimatedFees,
514
564
  challengeSolution: signedTx.challengeSolution,
@@ -20,7 +20,11 @@ import { BitcoinAbiTypes } from '../abi/BitcoinAbiTypes.js';
20
20
  import { BitcoinInterface } from '../abi/BitcoinInterface.js';
21
21
  import { BaseContractProperties } from '../abi/interfaces/BaseContractProperties.js';
22
22
  import { BitcoinAbiValue } from '../abi/interfaces/BitcoinAbiValue.js';
23
- import { BitcoinInterfaceAbi, EventBaseData, FunctionBaseData, } from '../abi/interfaces/BitcoinInterfaceAbi.js';
23
+ import {
24
+ BitcoinInterfaceAbi,
25
+ EventBaseData,
26
+ FunctionBaseData,
27
+ } from '../abi/interfaces/BitcoinInterfaceAbi.js';
24
28
  import { BlockGasParameters } from '../block/BlockGasParameters.js';
25
29
  import { DecodedCallResult } from '../common/CommonTypes.js';
26
30
  import { AbstractRpcProvider } from '../providers/AbstractRpcProvider.js';
@@ -141,7 +145,7 @@ export abstract class IBaseContract<T extends BaseContractProperties> implements
141
145
 
142
146
  return Promise.resolve(this.address);
143
147
  }
144
-
148
+
145
149
  /**
146
150
  * Sets the sender of the transaction.
147
151
  * @param {Address} sender The sender of the transaction.
@@ -288,9 +292,12 @@ export abstract class IBaseContract<T extends BaseContractProperties> implements
288
292
  return this[key];
289
293
  }
290
294
 
291
- private async getAddressOrThrow(address: string | Address, isContract: boolean): Promise<Address> {
295
+ private async getAddressOrThrow(
296
+ address: string | Address,
297
+ isContract: boolean,
298
+ ): Promise<Address> {
292
299
  const info = await this.provider.getPublicKeyInfo(address, isContract);
293
- if(!info) {
300
+ if (!info) {
294
301
  throw new Error(`Address ${address} not found on the network.`);
295
302
  }
296
303
 
@@ -2,6 +2,7 @@ import { Network } from '@btc-vision/bitcoin';
2
2
  import { Address, ChallengeSolution, IP2WSHAddress } from '@btc-vision/transaction';
3
3
  import { UTXO, UTXOs } from '../../bitcoin/UTXOs.js';
4
4
  import { BroadcastedTransaction } from '../../transactions/interfaces/BroadcastedTransaction.js';
5
+ import { BroadcastedTransactionPackage } from '../../transactions/interfaces/BroadcastedTransactionPackage.js';
5
6
  import { RequestUTXOsParamsWithAmount } from '../../utxos/interfaces/IUTXOsManager.js';
6
7
 
7
8
  /**
@@ -20,5 +21,9 @@ export interface IProviderForCallResult {
20
21
 
21
22
  getChallenge(): Promise<ChallengeSolution>;
22
23
  sendRawTransaction(tx: string, psbt: boolean): Promise<BroadcastedTransaction>;
24
+ sendRawTransactionPackage(
25
+ txs: string[],
26
+ isPackage?: boolean,
27
+ ): Promise<BroadcastedTransactionPackage>;
23
28
  getCSV1ForAddress(address: Address): IP2WSHAddress;
24
29
  }
package/src/opnet.ts CHANGED
@@ -102,6 +102,7 @@ export * from './storage/StoredValue.js';
102
102
  /** Interfaces */
103
103
  export * from './contracts/interfaces/IRawContract.js';
104
104
  export * from './transactions/interfaces/BroadcastedTransaction.js';
105
+ export * from './transactions/interfaces/BroadcastedTransactionPackage.js';
105
106
  export * from './transactions/interfaces/ITransaction.js';
106
107
  export * from './transactions/interfaces/ITransactionReceipt.js';
107
108
  export * from './transactions/metadata/TransactionReceipt.js';
@@ -23,12 +23,20 @@ import { TransactionOutputFlags } from '../contracts/enums/TransactionFlags.js';
23
23
  import { IAccessList } from '../contracts/interfaces/IAccessList.js';
24
24
  import { ICallRequestError, ICallResult } from '../contracts/interfaces/ICallResult.js';
25
25
  import { IRawContract } from '../contracts/interfaces/IRawContract.js';
26
- import { ParsedSimulatedTransaction, SimulatedTransaction, } from '../contracts/interfaces/SimulatedTransaction.js';
26
+ import {
27
+ ParsedSimulatedTransaction,
28
+ SimulatedTransaction,
29
+ } from '../contracts/interfaces/SimulatedTransaction.js';
27
30
  import { Epoch } from '../epoch/Epoch.js';
28
31
  import { EpochWithSubmissions } from '../epoch/EpochSubmission.js';
29
32
  import { EpochTemplate } from '../epoch/EpochTemplate.js';
30
33
  import { EpochSubmissionParams } from '../epoch/interfaces/EpochSubmissionParams.js';
31
- import { RawEpoch, RawEpochTemplate, RawEpochWithSubmissions, RawSubmittedEpoch, } from '../epoch/interfaces/IEpoch.js';
34
+ import {
35
+ RawEpoch,
36
+ RawEpochTemplate,
37
+ RawEpochWithSubmissions,
38
+ RawSubmittedEpoch,
39
+ } from '../epoch/interfaces/IEpoch.js';
32
40
  import { SubmittedEpoch } from '../epoch/SubmittedEpoch.js';
33
41
  import { OPNetTransactionTypes } from '../interfaces/opnet/OPNetTransactionTypes.js';
34
42
  import { MempoolTransactionData } from '../mempool/MempoolTransactionData.js';
@@ -36,6 +44,7 @@ import { MempoolTransactionParser } from '../mempool/MempoolTransactionParser.js
36
44
  import { IStorageValue } from '../storage/interfaces/IStorageValue.js';
37
45
  import { StoredValue } from '../storage/StoredValue.js';
38
46
  import { BroadcastedTransaction } from '../transactions/interfaces/BroadcastedTransaction.js';
47
+ import { BroadcastedTransactionPackage } from '../transactions/interfaces/BroadcastedTransactionPackage.js';
39
48
  import { ITransaction } from '../transactions/interfaces/ITransaction.js';
40
49
  import { ITransactionReceipt } from '../transactions/interfaces/ITransactionReceipt.js';
41
50
  import { TransactionReceipt } from '../transactions/metadata/TransactionReceipt.js';
@@ -54,7 +63,10 @@ import {
54
63
  JSONRpcResultError,
55
64
  } from './interfaces/JSONRpcResult.js';
56
65
  import { MempoolInfo } from './interfaces/mempool/MempoolInfo.js';
57
- import { IMempoolTransactionData, PendingTransactionsResult, } from './interfaces/mempool/MempoolTransactionData.js';
66
+ import {
67
+ IMempoolTransactionData,
68
+ PendingTransactionsResult,
69
+ } from './interfaces/mempool/MempoolTransactionData.js';
58
70
  import { AddressesInfo, IPublicKeyInfoResult } from './interfaces/PublicKeyInfo.js';
59
71
  import { ReorgInformation } from './interfaces/ReorgInformation.js';
60
72
 
@@ -124,8 +136,10 @@ export abstract class AbstractRpcProvider {
124
136
  try {
125
137
  const pubKeyInfo = await this.getPublicKeysInfo(address, isContract);
126
138
 
127
- return pubKeyInfo[address] ||
128
- pubKeyInfo[address.startsWith('0x') ? address.slice(2) : address];
139
+ return (
140
+ pubKeyInfo[address] ||
141
+ pubKeyInfo[address.startsWith('0x') ? address.slice(2) : address]
142
+ );
129
143
  } catch (e) {
130
144
  if (AddressVerificator.isValidPublicKey(address, this.network)) {
131
145
  return Address.fromString(address);
@@ -708,6 +722,39 @@ export abstract class AbstractRpcProvider {
708
722
  });
709
723
  }
710
724
 
725
+ /**
726
+ * Broadcast a package of raw transactions atomically.
727
+ * @description Submits an ordered array of raw transactions via Bitcoin Core's submitpackage
728
+ * RPC for atomic acceptance, or falls back to validated sequential broadcast.
729
+ * @param {string[]} txs The raw transactions to send as hex strings (max 25)
730
+ * @param {boolean} [isPackage=true] Whether to use atomic package submission (submitpackage)
731
+ * or validated sequential broadcast (testmempoolaccept + sendrawtransaction)
732
+ * @returns {Promise<BroadcastedTransactionPackage>} The result of the package broadcast
733
+ * @throws {Error} If something went wrong while broadcasting the package
734
+ */
735
+ public async sendRawTransactionPackage(
736
+ txs: string[],
737
+ isPackage: boolean = true,
738
+ ): Promise<BroadcastedTransactionPackage> {
739
+ if (!txs.length) {
740
+ throw new Error('sendRawTransactionPackage: txs array must not be empty');
741
+ }
742
+
743
+ for (let i = 0; i < txs.length; i++) {
744
+ if (!/^[0-9A-Fa-f]+$/.test(txs[i])) {
745
+ throw new Error(`sendRawTransactionPackage: txs[${i}] is not a valid hex string`);
746
+ }
747
+ }
748
+
749
+ const payload: JsonRpcPayload = this.buildJsonRpcPayload(
750
+ JSONRpcMethods.BROADCAST_TRANSACTION_PACKAGE,
751
+ [txs, isPackage],
752
+ );
753
+
754
+ const result: JsonRpcResult = await this.callPayloadSingle(payload);
755
+ return result.result as BroadcastedTransactionPackage;
756
+ }
757
+
711
758
  /**
712
759
  * Get block witnesses.
713
760
  * @description This method is used to get the witnesses of a block. This proves that the actions executed inside a block are valid and confirmed by the network. If the minimum number of witnesses are not met, the block is considered as potentially invalid.
@@ -424,6 +424,13 @@ export class WebSocketRpcProvider extends AbstractRpcProvider {
424
424
  3: params[1] ?? false,
425
425
  };
426
426
 
427
+ case JSONRpcMethods.BROADCAST_TRANSACTION_PACKAGE:
428
+ // BroadcastTransactionPackageRequest: requestId=1, transactions=2, isPackage=3
429
+ return {
430
+ 2: params[0],
431
+ 3: params[1] ?? true,
432
+ };
433
+
427
434
  case JSONRpcMethods.TRANSACTION_PREIMAGE:
428
435
  return {};
429
436
 
@@ -15,6 +15,7 @@ export enum JSONRpcMethods {
15
15
  /** Transactions */
16
16
  GET_TRANSACTION_BY_HASH = 'btc_getTransactionByHash',
17
17
  BROADCAST_TRANSACTION = 'btc_sendRawTransaction',
18
+ BROADCAST_TRANSACTION_PACKAGE = 'btc_sendRawTransactionPackage',
18
19
  TRANSACTION_PREIMAGE = 'btc_preimage',
19
20
 
20
21
  /** Addresses */
@@ -60,6 +60,12 @@ export const METHOD_MAPPINGS: Partial<Record<JSONRpcMethods, MethodMapping>> = {
60
60
  requestType: 'BroadcastTransactionRequest',
61
61
  responseType: 'BroadcastTransactionResponse',
62
62
  },
63
+ [JSONRpcMethods.BROADCAST_TRANSACTION_PACKAGE]: {
64
+ requestOpcode: WebSocketRequestOpcode.BROADCAST_TRANSACTION_PACKAGE,
65
+ responseOpcode: WebSocketResponseOpcode.BROADCAST_PACKAGE_RESULT,
66
+ requestType: 'BroadcastTransactionPackageRequest',
67
+ responseType: 'BroadcastTransactionPackageResponse',
68
+ },
63
69
  [JSONRpcMethods.TRANSACTION_PREIMAGE]: {
64
70
  requestOpcode: WebSocketRequestOpcode.GET_PREIMAGE,
65
71
  responseOpcode: WebSocketResponseOpcode.PREIMAGE,
@@ -19,6 +19,7 @@ export enum WebSocketRequestOpcode {
19
19
  GET_TRANSACTION_BY_HASH = 0x20,
20
20
  GET_TRANSACTION_RECEIPT = 0x21,
21
21
  BROADCAST_TRANSACTION = 0x22,
22
+ BROADCAST_TRANSACTION_PACKAGE = 0x27,
22
23
  GET_PREIMAGE = 0x23,
23
24
 
24
25
  // Mempool Methods (0x24 - 0x2F)
@@ -80,6 +81,7 @@ export enum WebSocketResponseOpcode {
80
81
  TRANSACTION = 0xa0,
81
82
  TRANSACTION_RECEIPT = 0xa1,
82
83
  BROADCAST_RESULT = 0xa2,
84
+ BROADCAST_PACKAGE_RESULT = 0xa7,
83
85
  PREIMAGE = 0xa3,
84
86
  /** Response containing aggregate mempool statistics. */
85
87
  MEMPOOL_INFO = 0xa4,
@@ -0,0 +1,72 @@
1
+ export interface SequentialBroadcastTxResult {
2
+ /** The txid of the transaction. */
3
+ readonly txid: string;
4
+
5
+ /** Whether the individual transaction was successfully broadcast. */
6
+ readonly success: boolean;
7
+
8
+ /** Error message if this transaction failed. */
9
+ readonly error?: string;
10
+
11
+ /** Number of peers that received the transaction. */
12
+ readonly peers?: number;
13
+ }
14
+
15
+ export interface BroadcastedTransactionPackage {
16
+ /** Whether the overall package broadcast succeeded. */
17
+ readonly success: boolean;
18
+
19
+ /** Error message if the broadcast failed. */
20
+ readonly error?: string;
21
+
22
+ /** Present when testMempoolAccept was used (sequential or single tx path). */
23
+ readonly testResults?: readonly TestMempoolAcceptResult[];
24
+
25
+ /** Present when submitPackage was used successfully. */
26
+ readonly packageResult?: PackageResult;
27
+
28
+ /** Per-transaction results for sequential or single tx broadcasts. */
29
+ readonly sequentialResults?: readonly SequentialBroadcastTxResult[];
30
+
31
+ /** True when submitPackage failed and the node fell back to sequential broadcast. */
32
+ readonly fellBackToSequential?: boolean;
33
+ }
34
+
35
+ export interface TestMempoolAcceptResult {
36
+ readonly txid: string;
37
+ readonly wtxid: string;
38
+ readonly allowed?: boolean;
39
+ readonly vsize?: number;
40
+ readonly packageError?: string;
41
+ readonly rejectReason?: string;
42
+ readonly rejectDetails?: string;
43
+ readonly fees?: TestMempoolAcceptFees;
44
+ }
45
+
46
+ export interface TestMempoolAcceptFees {
47
+ readonly base: number;
48
+ readonly effectiveFeerate: number;
49
+ readonly effectiveIncludes: readonly string[];
50
+ }
51
+
52
+ export interface PackageTxResult {
53
+ readonly txid: string;
54
+ readonly otherWtxid?: string;
55
+ readonly vsize?: number;
56
+ readonly fees?: PackageTxFees;
57
+ readonly error?: string;
58
+ }
59
+
60
+ export interface PackageTxFees {
61
+ readonly base: number;
62
+ readonly effectiveFeerate?: number;
63
+ readonly effectiveIncludes?: readonly string[];
64
+ }
65
+
66
+ export interface PackageResult {
67
+ readonly packageMsg: string;
68
+ readonly txResults: {
69
+ readonly [wtxid: string]: PackageTxResult;
70
+ };
71
+ readonly replacedTransactions?: readonly string[];
72
+ }
@@ -40,7 +40,7 @@ interface AddressData {
40
40
  */
41
41
  export class UTXOsManager {
42
42
  /**
43
- * Holds all address-specific data so we dont mix up UTXOs between addresses/wallets.
43
+ * Holds all address-specific data so we don't mix up UTXOs between addresses/wallets.
44
44
  */
45
45
  private dataByAddress: Record<string, AddressData> = {};
46
46
 
@@ -97,7 +97,7 @@ export class UTXOsManager {
97
97
  );
98
98
  }
99
99
 
100
- // Push the new UTXOs into this addresss pending and set their depth
100
+ // Push the new UTXOs into this address's pending and set their depth
101
101
  for (const nu of newUTXOs) {
102
102
  addressData.pendingUTXOs.push(nu);
103
103
  addressData.pendingUtxoDepth[utxoKey(nu)] = newDepth;
@@ -212,11 +212,14 @@ export class UTXOsManager {
212
212
  * Fetch UTXOs for a specific amount needed, from a single address,
213
213
  * merging from pending and confirmed UTXOs.
214
214
  *
215
+ * Prioritizes normal UTXOs first, only falling back to CSV UTXOs
216
+ * if the normal ones cannot cover the requested amount.
217
+ *
215
218
  * @param {object} options
216
219
  * @param {string} options.address The address to fetch UTXOs for
217
220
  * @param {bigint} options.amount The needed amount
218
221
  * @param {boolean} [options.optimize=true] Optimize the UTXOs
219
- * @param {boolean} [options.csvAddress] Use CSV UTXOs in priority
222
+ * @param {boolean} [options.csvAddress] Use CSV UTXOs as fallback
220
223
  * @param {boolean} [options.mergePendingUTXOs=true] Merge pending
221
224
  * @param {boolean} [options.filterSpentUTXOs=true] Filter out spent
222
225
  * @param {boolean} [options.throwErrors=false] Throw error if insufficient
@@ -235,52 +238,46 @@ export class UTXOsManager {
235
238
  maxUTXOs = 5000,
236
239
  throwIfUTXOsLimitReached = false,
237
240
  }: RequestUTXOsParamsWithAmount): Promise<UTXOs> {
238
- const utxosPromises: Promise<UTXO[]>[] = [];
239
-
240
- if (csvAddress) {
241
- utxosPromises.push(
242
- this.getUTXOs({
243
- address: csvAddress,
244
- optimize: true,
245
- mergePendingUTXOs: false,
246
- filterSpentUTXOs: true,
247
- olderThan: 1n,
248
- isCSV: true,
249
- }),
250
- );
251
- }
252
-
253
- utxosPromises.push(
254
- this.getUTXOs({
255
- address,
256
- optimize,
257
- mergePendingUTXOs,
258
- filterSpentUTXOs,
259
- olderThan,
260
- }),
261
- );
262
-
263
- const combinedUTXOs: UTXOs = (await Promise.all(utxosPromises)).flat();
264
- const utxoUntilAmount: UTXOs = [];
265
-
241
+ const selected: UTXOs = [];
266
242
  let currentValue = 0n;
267
- for (const utxo of combinedUTXOs) {
268
- if (maxUTXOs && utxoUntilAmount.length >= maxUTXOs) {
269
- if (throwIfUTXOsLimitReached) {
270
- throw new Error(
271
- `Woah. You must consolidate your UTXOs (${combinedUTXOs.length})! This transaction is too large.`,
272
- );
273
- }
274
243
 
275
- break;
276
- }
244
+ // Fetch and greedily select from normal UTXOs first
245
+ const normalUTXOs: UTXOs = await this.getUTXOs({
246
+ address,
247
+ optimize,
248
+ mergePendingUTXOs,
249
+ filterSpentUTXOs,
250
+ olderThan,
251
+ });
277
252
 
278
- utxoUntilAmount.push(utxo);
279
- currentValue += utxo.value;
253
+ currentValue = this.selectUTXOsGreedily(
254
+ normalUTXOs,
255
+ selected,
256
+ currentValue,
257
+ amount,
258
+ maxUTXOs,
259
+ throwIfUTXOsLimitReached,
260
+ );
280
261
 
281
- if (currentValue >= amount) {
282
- break;
283
- }
262
+ // Fall back to CSV UTXOs only if normal ones were insufficient
263
+ if (currentValue < amount && csvAddress) {
264
+ const csvUTXOs: UTXOs = await this.getUTXOs({
265
+ address: csvAddress,
266
+ optimize: true,
267
+ mergePendingUTXOs: false,
268
+ filterSpentUTXOs: true,
269
+ olderThan: 1n,
270
+ isCSV: true,
271
+ });
272
+
273
+ currentValue = this.selectUTXOsGreedily(
274
+ csvUTXOs,
275
+ selected,
276
+ currentValue,
277
+ amount,
278
+ maxUTXOs,
279
+ throwIfUTXOsLimitReached,
280
+ );
284
281
  }
285
282
 
286
283
  if (currentValue < amount && throwErrors) {
@@ -289,7 +286,7 @@ export class UTXOsManager {
289
286
  );
290
287
  }
291
288
 
292
- return utxoUntilAmount;
289
+ return selected;
293
290
  }
294
291
 
295
292
  /**
@@ -388,6 +385,45 @@ export class UTXOsManager {
388
385
  return result;
389
386
  }
390
387
 
388
+ /**
389
+ * Sort UTXOs by value descending and greedily append to `selected` until
390
+ * `currentValue >= amount` or the pool is exhausted. Mutates `candidates`
391
+ * (sort in-place) and `selected` (pushes chosen UTXOs). Returns the
392
+ * updated cumulative value.
393
+ */
394
+ private selectUTXOsGreedily(
395
+ candidates: UTXOs,
396
+ selected: UTXOs,
397
+ currentValue: bigint,
398
+ amount: bigint,
399
+ maxUTXOs: number,
400
+ throwIfLimitReached: boolean,
401
+ ): bigint {
402
+ candidates.sort((a, b) => {
403
+ if (b.value > a.value) return 1;
404
+ if (b.value < a.value) return -1;
405
+ return 0;
406
+ });
407
+
408
+ for (const utxo of candidates) {
409
+ if (currentValue >= amount) break;
410
+
411
+ if (maxUTXOs && selected.length >= maxUTXOs) {
412
+ if (throwIfLimitReached) {
413
+ throw new Error(
414
+ `Woah. You must consolidate your UTXOs (${candidates.length + selected.length})! This transaction is too large.`,
415
+ );
416
+ }
417
+ break;
418
+ }
419
+
420
+ selected.push(utxo);
421
+ currentValue += utxo.value;
422
+ }
423
+
424
+ return currentValue;
425
+ }
426
+
391
427
  /**
392
428
  * Fetch UTXOs for multiple addresses in a single batch RPC call.
393
429
  * @private