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.
- package/CHANGELOG.md +5 -0
- package/browser/_version.d.ts +1 -1
- package/browser/contracts/interfaces/IProviderForCallResult.d.ts +2 -0
- package/browser/index.js +174 -63
- package/browser/opnet.d.ts +1 -0
- package/browser/providers/AbstractRpcProvider.d.ts +2 -0
- package/browser/providers/interfaces/JSONRpcMethods.d.ts +1 -0
- package/browser/providers/websocket/types/WebSocketOpcodes.d.ts +2 -0
- package/browser/transactions/interfaces/BroadcastedTransactionPackage.d.ts +48 -0
- package/browser/utxos/UTXOsManager.d.ts +1 -0
- package/build/_version.d.ts +1 -1
- package/build/_version.js +1 -1
- package/build/contracts/CallResult.js +54 -19
- package/build/contracts/interfaces/IProviderForCallResult.d.ts +2 -0
- package/build/opnet.d.ts +1 -0
- package/build/opnet.js +1 -0
- package/build/providers/AbstractRpcProvider.d.ts +2 -0
- package/build/providers/AbstractRpcProvider.js +15 -2
- package/build/providers/WebsocketRpcProvider.js +5 -0
- package/build/providers/interfaces/JSONRpcMethods.d.ts +1 -0
- package/build/providers/interfaces/JSONRpcMethods.js +1 -0
- package/build/providers/websocket/MethodMapping.js +6 -0
- package/build/providers/websocket/types/WebSocketOpcodes.d.ts +2 -0
- package/build/providers/websocket/types/WebSocketOpcodes.js +2 -0
- package/build/transactions/interfaces/BroadcastedTransactionPackage.d.ts +48 -0
- package/build/transactions/interfaces/BroadcastedTransactionPackage.js +1 -0
- package/build/tsconfig.build.tsbuildinfo +1 -1
- package/build/utxos/UTXOsManager.d.ts +1 -0
- package/build/utxos/UTXOsManager.js +37 -28
- package/docs/api-reference/provider-api.md +16 -0
- package/docs/api-reference/types-interfaces.md +91 -0
- package/docs/api-reference/utxo-manager-api.md +4 -2
- package/docs/svg/tx-broadcast-flow.svg +201 -0
- package/docs/transactions/broadcasting.md +124 -9
- package/package.json +2 -3
- package/src/_version.ts +1 -1
- package/src/contracts/CallResult.ts +82 -32
- package/src/contracts/Contract.ts +11 -4
- package/src/contracts/interfaces/IProviderForCallResult.ts +5 -0
- package/src/opnet.ts +1 -0
- package/src/providers/AbstractRpcProvider.ts +52 -5
- package/src/providers/WebsocketRpcProvider.ts +7 -0
- package/src/providers/interfaces/JSONRpcMethods.ts +1 -0
- package/src/providers/websocket/MethodMapping.ts +6 -0
- package/src/providers/websocket/types/WebSocketOpcodes.ts +2 -0
- package/src/transactions/interfaces/BroadcastedTransactionPackage.ts +72 -0
- 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 (
|
|
471
|
-
|
|
472
|
-
|
|
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 (!
|
|
481
|
-
throw new Error(`Error sending transaction: ${
|
|
496
|
+
if (!tx || tx.error) {
|
|
497
|
+
throw new Error(`Error sending transaction: ${tx?.error || 'Unknown error'}`);
|
|
482
498
|
}
|
|
483
499
|
|
|
484
|
-
if (!
|
|
485
|
-
throw new Error(
|
|
500
|
+
if (!tx.result) {
|
|
501
|
+
throw new Error('No transaction ID returned');
|
|
486
502
|
}
|
|
487
|
-
}
|
|
488
503
|
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
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 (!
|
|
495
|
-
throw new 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
|
|
499
|
-
|
|
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
|
-
|
|
503
|
-
|
|
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:
|
|
511
|
-
peerAcknowledgements:
|
|
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 {
|
|
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(
|
|
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 {
|
|
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 {
|
|
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 {
|
|
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
|
|
128
|
-
pubKeyInfo[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 don
|
|
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 address
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
279
|
-
|
|
253
|
+
currentValue = this.selectUTXOsGreedily(
|
|
254
|
+
normalUTXOs,
|
|
255
|
+
selected,
|
|
256
|
+
currentValue,
|
|
257
|
+
amount,
|
|
258
|
+
maxUTXOs,
|
|
259
|
+
throwIfUTXOsLimitReached,
|
|
260
|
+
);
|
|
280
261
|
|
|
281
|
-
|
|
282
|
-
|
|
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
|
|
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
|