@atomicfinance/bitcoin-ddk-provider 4.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,4294 @@
1
+ import Provider from '@atomicfinance/provider';
2
+ import {
3
+ AdaptorSignature,
4
+ Address,
5
+ Amount,
6
+ CalculateEcSignatureRequest,
7
+ CreateRawTransactionRequest,
8
+ CreateSignatureHashRequest,
9
+ DdkDlcInputInfo,
10
+ DdkDlcTransactions,
11
+ DdkInterface,
12
+ DdkOracleInfo,
13
+ DdkTransaction,
14
+ DlcInputInfo,
15
+ DlcInputInfoRequest,
16
+ Input,
17
+ InputSupplementationMode,
18
+ Messages,
19
+ PartyParams,
20
+ Payout,
21
+ PayoutRequest,
22
+ Utxo,
23
+ VerifySignatureRequest,
24
+ } from '@atomicfinance/types';
25
+ import { sleep } from '@atomicfinance/utils';
26
+ import { Script, Sequence, Tx } from '@node-dlc/bitcoin';
27
+ import { StreamReader } from '@node-dlc/bufio';
28
+ import { BatchDlcTxBuilder } from '@node-dlc/core';
29
+ import {
30
+ DualClosingTxFinalizer,
31
+ DualFundingTxFinalizer,
32
+ groupByIgnoringDigits,
33
+ HyperbolaPayoutCurve,
34
+ PolynomialPayoutCurve,
35
+ roundPayout,
36
+ } from '@node-dlc/core';
37
+ import { hash160, sha256, xor } from '@node-dlc/crypto';
38
+ import {
39
+ CetAdaptorSignatures,
40
+ ContractDescriptor,
41
+ ContractDescriptorType,
42
+ ContractInfo,
43
+ ContractInfoType,
44
+ DigitDecompositionEventDescriptor,
45
+ DisjointContractInfo,
46
+ DlcAccept,
47
+ DlcClose,
48
+ DlcCloseMetadata,
49
+ DlcInput,
50
+ DlcOffer,
51
+ DlcSign,
52
+ DlcTransactions,
53
+ EnumeratedDescriptor,
54
+ EnumEventDescriptor,
55
+ F64,
56
+ FundingInput,
57
+ FundingSignatures,
58
+ HyperbolaPayoutCurvePiece,
59
+ MessageType,
60
+ MultiOracleInfo,
61
+ NumericalDescriptor,
62
+ OracleAttestation,
63
+ OracleEvent,
64
+ OracleInfo,
65
+ PayoutCurvePieceType,
66
+ PayoutFunction,
67
+ PayoutFunctionV0,
68
+ PolynomialPayoutCurvePiece,
69
+ ScriptWitnessV0,
70
+ SingleContractInfo,
71
+ SingleOracleInfo,
72
+ } from '@node-dlc/messaging';
73
+ import assert from 'assert';
74
+ import BigNumber from 'bignumber.js';
75
+ import { BitcoinNetwork, chainHashFromNetwork } from 'bitcoin-networks';
76
+ import {
77
+ address,
78
+ networks,
79
+ payments,
80
+ Psbt,
81
+ script,
82
+ Transaction as btTransaction,
83
+ } from 'bitcoinjs-lib';
84
+ import crypto from 'crypto';
85
+ import { ECPairFactory, ECPairInterface } from 'ecpair';
86
+ import * as ecc from 'tiny-secp256k1';
87
+
88
+ import { checkTypes, generateSerialId, outputsToPayouts } from './utils/Utils';
89
+
90
+ const ECPair = ECPairFactory(ecc);
91
+
92
+ export default class BitcoinDdkProvider extends Provider {
93
+ private _network: BitcoinNetwork;
94
+ private _ddk: DdkInterface;
95
+
96
+ constructor(network: BitcoinNetwork, ddkLib: DdkInterface) {
97
+ super();
98
+ this._network = network;
99
+ this._ddk = ddkLib;
100
+ }
101
+
102
+ public async DdkLoaded() {
103
+ while (!this._ddk) {
104
+ await sleep(10);
105
+ }
106
+ }
107
+
108
+ /**
109
+ * Find private key for DLC funding pubkey by deriving wallet addresses
110
+ */
111
+ private async findDlcFundingPrivateKey(
112
+ localFundPubkey: string,
113
+ remoteFundPubkey: string,
114
+ ): Promise<string> {
115
+ const targetPubkeys = [localFundPubkey, remoteFundPubkey];
116
+
117
+ // First check existing wallet addresses
118
+ const addresses = await this.getMethod('getAddresses')();
119
+
120
+ for (const addressInfo of addresses) {
121
+ if (addressInfo.derivationPath) {
122
+ try {
123
+ const keyPair = await this.getMethod('keyPair')(
124
+ addressInfo.derivationPath,
125
+ );
126
+ const pubkey = Buffer.from(keyPair.publicKey);
127
+ const pubkeyHex = pubkey.toString('hex');
128
+
129
+ if (targetPubkeys.includes(pubkeyHex)) {
130
+ return Buffer.from(keyPair.privateKey).toString('hex');
131
+ }
132
+ } catch {
133
+ continue;
134
+ }
135
+ }
136
+ }
137
+
138
+ // If not found in existing addresses, do comprehensive search
139
+ // For DLC splicing, funding pubkeys can be at much higher derivation paths
140
+ console.log('Searching extensively for DLC funding private key...');
141
+
142
+ for (const isChange of [false, true]) {
143
+ for (let i = 0; i < 1000; i++) {
144
+ // Search up to 1000 addresses for DLC keys
145
+ try {
146
+ const address = await this.client.wallet.getAddresses(i, 1, isChange);
147
+ if (address && address.length > 0) {
148
+ const addressInfo = address[0];
149
+
150
+ if (addressInfo.derivationPath) {
151
+ const keyPair = await this.getMethod('keyPair')(
152
+ addressInfo.derivationPath,
153
+ );
154
+ const pubkey = Buffer.from(keyPair.publicKey);
155
+ const pubkeyHex = pubkey.toString('hex');
156
+
157
+ if (targetPubkeys.includes(pubkeyHex)) {
158
+ console.log(
159
+ `Found DLC funding key at derivation path: ${addressInfo.derivationPath}`,
160
+ );
161
+ return Buffer.from(keyPair.privateKey).toString('hex');
162
+ }
163
+ }
164
+ }
165
+ } catch {
166
+ continue;
167
+ }
168
+ }
169
+ }
170
+
171
+ throw new Error(
172
+ `Could not find private key for DLC funding pubkeys: local=${localFundPubkey}, remote=${remoteFundPubkey}`,
173
+ );
174
+ }
175
+
176
+ private async GetPrivKeysForInputs(inputs: Input[]): Promise<string[]> {
177
+ const privKeys: string[] = [];
178
+
179
+ for (let i = 0; i < inputs.length; i++) {
180
+ const input = inputs[i];
181
+
182
+ if (input.isDlcInput()) {
183
+ // Handle DLC input - use the dedicated method to find the funding private key
184
+ const dlcInput = input.dlcInput!;
185
+ const foundPrivKey = await this.findDlcFundingPrivateKey(
186
+ dlcInput.localFundPubkey,
187
+ dlcInput.remoteFundPubkey,
188
+ );
189
+ privKeys.push(foundPrivKey);
190
+ } else {
191
+ // Handle regular input
192
+ let derivationPath = input.derivationPath;
193
+
194
+ if (!derivationPath) {
195
+ try {
196
+ derivationPath = (
197
+ await this.getMethod('getWalletAddress')(input.address)
198
+ ).derivationPath;
199
+ } catch (error) {
200
+ throw new Error(
201
+ `Unable to find address ${input.address} in wallet. ` +
202
+ `This may happen when using derivation paths outside the normal range. ` +
203
+ `Error: ${error.message}`,
204
+ );
205
+ }
206
+ }
207
+
208
+ const keyPair = await this.getMethod('keyPair')(derivationPath);
209
+ const privKey = Buffer.from(keyPair.__D).toString('hex');
210
+ privKeys.push(privKey);
211
+ }
212
+ }
213
+
214
+ return privKeys;
215
+ }
216
+
217
+ async GetCfdNetwork(): Promise<string> {
218
+ const network = await this.getConnectedNetwork();
219
+
220
+ switch (network.name) {
221
+ case 'bitcoin_testnet':
222
+ return 'testnet';
223
+ case 'bitcoin_regtest':
224
+ return 'regtest';
225
+ default:
226
+ return 'bitcoin';
227
+ }
228
+ }
229
+
230
+ /**
231
+ * Get inputs for amount with explicit supplementation control
232
+ */
233
+ async GetInputsForAmountWithMode(
234
+ amounts: bigint[],
235
+ feeRatePerVb: bigint,
236
+ fixedInputs: Input[] = [],
237
+ supplementation: InputSupplementationMode = InputSupplementationMode.Required,
238
+ ): Promise<Input[]> {
239
+ if (amounts.length === 0) return [];
240
+
241
+ // For "none" mode, use exactly the provided inputs
242
+ if (supplementation === InputSupplementationMode.None) {
243
+ return fixedInputs;
244
+ }
245
+
246
+ // For "required" and "optional" modes, attempt supplementation
247
+ const fixedUtxos = fixedInputs.map((input) => input.toUtxo());
248
+
249
+ try {
250
+ const inputsForAmount: InputsForDualAmountResponse = await this.getMethod(
251
+ 'getInputsForDualFunding',
252
+ )(amounts, feeRatePerVb, fixedUtxos);
253
+
254
+ // Convert UTXO objects to Input class instances
255
+ return inputsForAmount.inputs.map((utxo) => Input.fromUTXO(utxo));
256
+ } catch (e) {
257
+ const errorMessage = e instanceof Error ? e.message : 'Unknown error';
258
+
259
+ if (supplementation === InputSupplementationMode.Required) {
260
+ throw Error(
261
+ `Not enough balance GetInputsForAmountWithMode. Error: ${errorMessage}`,
262
+ );
263
+ } else {
264
+ // Optional mode: fallback to provided inputs
265
+ return fixedInputs;
266
+ }
267
+ }
268
+ }
269
+
270
+ async GetInputsForAmount(
271
+ amounts: bigint[],
272
+ feeRatePerVb: bigint,
273
+ fixedInputs: Input[] = [],
274
+ ): Promise<Input[]> {
275
+ if (amounts.length === 0) return [];
276
+
277
+ const fixedUtxos = fixedInputs.map((input) => input.toUtxo());
278
+
279
+ let inputs: Input[];
280
+ try {
281
+ const inputsForAmount: InputsForDualAmountResponse = await this.getMethod(
282
+ 'getInputsForDualFunding',
283
+ )(amounts, feeRatePerVb, fixedUtxos);
284
+
285
+ // Convert UTXO objects to Input class instances
286
+ inputs = inputsForAmount.inputs.map((utxo) => Input.fromUTXO(utxo));
287
+ } catch (e) {
288
+ const errorMessage = e instanceof Error ? e.message : 'Unknown error';
289
+ if (fixedInputs.length === 0) {
290
+ throw Error(
291
+ `Not enough balance getInputsForAmount. Error: ${errorMessage}`,
292
+ );
293
+ } else {
294
+ inputs = fixedInputs;
295
+ }
296
+ }
297
+
298
+ return inputs;
299
+ }
300
+
301
+ private async Initialize(
302
+ collateral: bigint,
303
+ feeRatePerVb: bigint,
304
+ fixedInputs: Input[],
305
+ inputSupplementationMode: InputSupplementationMode = InputSupplementationMode.Required,
306
+ ): Promise<InitializeResponse> {
307
+ const network = await this.getConnectedNetwork();
308
+ const payoutAddress: Address =
309
+ await this.client.wallet.getUnusedAddress(false);
310
+ const payoutSPK: Buffer = address.toOutputScript(
311
+ payoutAddress.address,
312
+ network,
313
+ );
314
+ const changeAddress: Address =
315
+ await this.client.wallet.getUnusedAddress(true);
316
+ const changeSPK: Buffer = address.toOutputScript(
317
+ changeAddress.address,
318
+ network,
319
+ );
320
+
321
+ const fundingAddress: Address =
322
+ await this.client.wallet.getUnusedAddress(false);
323
+ const fundingPubKey: Buffer = Buffer.from(fundingAddress.publicKey, 'hex');
324
+
325
+ if (fundingAddress.address === payoutAddress.address)
326
+ throw Error('Address reuse');
327
+
328
+ const inputs: Input[] = await this.GetInputsForAmountWithMode(
329
+ [collateral],
330
+ feeRatePerVb,
331
+ fixedInputs,
332
+ inputSupplementationMode,
333
+ );
334
+ const fundingInputs: FundingInput[] = await Promise.all(
335
+ inputs.map(async (input) => {
336
+ return this.inputToFundingInput(input);
337
+ }),
338
+ );
339
+
340
+ const payoutSerialId: bigint = generateSerialId();
341
+ const changeSerialId: bigint = generateSerialId();
342
+
343
+ return {
344
+ fundingPubKey,
345
+ payoutSPK,
346
+ payoutSerialId,
347
+ fundingInputs,
348
+ changeSPK,
349
+ changeSerialId,
350
+ };
351
+ }
352
+
353
+ /**
354
+ * TODO: Add GetPayoutFromOutcomes
355
+ *
356
+ * private GetPayoutsFromOutcomes(
357
+ * contractDescriptor: ContractDescriptorV0,
358
+ * totalCollateral: bigint,
359
+ * ): PayoutRequest[] {}
360
+ */
361
+
362
+ private GetPayoutsFromPayoutFunction(
363
+ dlcOffer: DlcOffer,
364
+ contractDescriptor: NumericalDescriptor,
365
+ oracleInfo: OracleInfo,
366
+ totalCollateral: bigint,
367
+ ): GetPayoutsResponse {
368
+ const payoutFunction = contractDescriptor.payoutFunction as PayoutFunction;
369
+ if (payoutFunction.payoutFunctionPieces.length === 0)
370
+ throw Error('PayoutFunction must have at least once PayoutCurvePiece');
371
+ if (payoutFunction.payoutFunctionPieces.length > 1)
372
+ throw Error('More than one PayoutCurvePiece not supported');
373
+ const payoutCurvePiece = payoutFunction.payoutFunctionPieces[0]
374
+ .payoutCurvePiece as HyperbolaPayoutCurvePiece;
375
+ if (
376
+ payoutCurvePiece.payoutCurvePieceType !== PayoutCurvePieceType.Hyperbola
377
+ )
378
+ throw Error('Must be HyperbolaPayoutCurvePiece');
379
+ if (!payoutCurvePiece.b.eq(F64.ZERO) || !payoutCurvePiece.c.eq(F64.ZERO))
380
+ throw Error('b and c HyperbolaPayoutCurvePiece values must be 0');
381
+ // Cast to SingleOracleInfo to access announcement property
382
+ const singleOracleInfo = oracleInfo as SingleOracleInfo;
383
+ const eventDescriptor = singleOracleInfo.announcement.oracleEvent
384
+ .eventDescriptor as DigitDecompositionEventDescriptor;
385
+ if (eventDescriptor.type !== MessageType.DigitDecompositionEventDescriptor)
386
+ throw Error('Only DigitDecomposition Oracle Events supported');
387
+
388
+ const roundingIntervals = contractDescriptor.roundingIntervals;
389
+ const cetPayouts = HyperbolaPayoutCurve.computePayouts(
390
+ payoutFunction,
391
+ totalCollateral,
392
+ roundingIntervals,
393
+ );
394
+
395
+ const payoutGroups: PayoutGroup[] = [];
396
+ cetPayouts.forEach((p) => {
397
+ payoutGroups.push({
398
+ payout: p.payout,
399
+ groups: groupByIgnoringDigits(
400
+ p.indexFrom,
401
+ p.indexTo,
402
+ eventDescriptor.base,
403
+ contractDescriptor.numDigits,
404
+ ),
405
+ });
406
+ });
407
+
408
+ const rValuesMessagesList = this.GenerateMessages(singleOracleInfo);
409
+
410
+ const { payouts, messagesList } = outputsToPayouts(
411
+ payoutGroups,
412
+ rValuesMessagesList,
413
+ dlcOffer.offerCollateral,
414
+ dlcOffer.contractInfo.totalCollateral - dlcOffer.offerCollateral,
415
+ true,
416
+ );
417
+
418
+ return { payouts, payoutGroups, messagesList };
419
+ }
420
+
421
+ private GetPayoutsFromPolynomialPayoutFunction(
422
+ dlcOffer: DlcOffer,
423
+ contractDescriptor: NumericalDescriptor,
424
+ oracleInfo: SingleOracleInfo,
425
+ totalCollateral: bigint,
426
+ ): GetPayoutsResponse {
427
+ const payoutFunction = contractDescriptor.payoutFunction as PayoutFunction;
428
+ if (payoutFunction.payoutFunctionPieces.length === 0)
429
+ throw Error('PayoutFunction must have at least once PayoutCurvePiece');
430
+ for (const piece of payoutFunction.payoutFunctionPieces) {
431
+ if (
432
+ piece.payoutCurvePiece.type !== MessageType.PolynomialPayoutCurvePiece
433
+ )
434
+ throw Error('Must be PolynomialPayoutCurvePiece');
435
+ }
436
+ const eventDescriptor = oracleInfo.announcement.oracleEvent
437
+ .eventDescriptor as DigitDecompositionEventDescriptor;
438
+ if (eventDescriptor.type !== MessageType.DigitDecompositionEventDescriptor)
439
+ throw Error('Only DigitDecomposition Oracle Events supported');
440
+
441
+ const roundingIntervals = contractDescriptor.roundingIntervals;
442
+ const cetPayouts = PolynomialPayoutCurve.computePayouts(
443
+ payoutFunction,
444
+ totalCollateral,
445
+ roundingIntervals,
446
+ );
447
+
448
+ const payoutGroups: PayoutGroup[] = [];
449
+ cetPayouts.forEach((p) => {
450
+ payoutGroups.push({
451
+ payout: p.payout,
452
+ groups: groupByIgnoringDigits(
453
+ p.indexFrom,
454
+ p.indexTo,
455
+ eventDescriptor.base,
456
+ contractDescriptor.numDigits,
457
+ ),
458
+ });
459
+ });
460
+
461
+ const rValuesMessagesList = this.GenerateMessages(oracleInfo);
462
+
463
+ const { payouts, messagesList } = outputsToPayouts(
464
+ payoutGroups,
465
+ rValuesMessagesList,
466
+ dlcOffer.offerCollateral,
467
+ dlcOffer.contractInfo.totalCollateral - dlcOffer.offerCollateral,
468
+ true,
469
+ );
470
+
471
+ return { payouts, payoutGroups, messagesList };
472
+ }
473
+
474
+ private GetPayouts(dlcOffer: DlcOffer): GetPayoutsResponse[] {
475
+ const contractInfo = dlcOffer.contractInfo;
476
+ const totalCollateral = contractInfo.totalCollateral;
477
+ const contractOraclePairs = this.GetContractOraclePairs(contractInfo);
478
+
479
+ const payoutResponses = contractOraclePairs.map(
480
+ ({ contractDescriptor, oracleInfo }) =>
481
+ this.GetPayoutsFromContractDescriptor(
482
+ dlcOffer,
483
+ contractDescriptor,
484
+ oracleInfo,
485
+ totalCollateral,
486
+ ),
487
+ );
488
+
489
+ return payoutResponses;
490
+ }
491
+
492
+ private FlattenPayouts(payoutResponses: GetPayoutsResponse[]) {
493
+ return payoutResponses.reduce(
494
+ (acc, { payouts, payoutGroups, messagesList }) => {
495
+ return {
496
+ payouts: acc.payouts.concat(payouts),
497
+ payoutGroups: acc.payoutGroups.concat(payoutGroups),
498
+ messagesList: acc.messagesList.concat(messagesList),
499
+ };
500
+ },
501
+ );
502
+ }
503
+
504
+ private GetIndicesFromPayouts(payoutResponses: GetPayoutsResponse[]) {
505
+ return payoutResponses.reduce(
506
+ (prev, acc) => {
507
+ return prev.concat({
508
+ startingMessagesIndex:
509
+ prev[prev.length - 1].startingMessagesIndex +
510
+ acc.messagesList.length,
511
+ startingPayoutGroupsIndex:
512
+ prev[prev.length - 1].startingPayoutGroupsIndex +
513
+ acc.payoutGroups.length,
514
+ });
515
+ },
516
+ [{ startingMessagesIndex: 0, startingPayoutGroupsIndex: 0 }],
517
+ );
518
+ }
519
+
520
+ private GetPayoutsFromEnumeratedDescriptor(
521
+ dlcOffer: DlcOffer,
522
+ contractDescriptor: EnumeratedDescriptor,
523
+ oracleInfo: OracleInfo,
524
+ totalCollateral: bigint,
525
+ ): GetPayoutsResponse {
526
+ const payoutGroups: PayoutGroup[] = [];
527
+ const rValuesMessagesList = this.GenerateMessages(
528
+ oracleInfo as SingleOracleInfo,
529
+ );
530
+
531
+ // For enumerated descriptors, each outcome creates one payout
532
+ // Each outcome maps to one index in the oracle's possible outcomes
533
+ contractDescriptor.outcomes.forEach((outcome, index) => {
534
+ payoutGroups.push({
535
+ payout: outcome.localPayout,
536
+ groups: [[index]], // Simple index-based grouping for enum outcomes
537
+ });
538
+ });
539
+
540
+ const { payouts, messagesList } = outputsToPayouts(
541
+ payoutGroups,
542
+ rValuesMessagesList,
543
+ dlcOffer.offerCollateral,
544
+ totalCollateral - dlcOffer.offerCollateral,
545
+ true,
546
+ );
547
+
548
+ return { payouts, payoutGroups, messagesList };
549
+ }
550
+
551
+ private GetPayoutsFromContractDescriptor(
552
+ dlcOffer: DlcOffer,
553
+ contractDescriptor: ContractDescriptor,
554
+ oracleInfo: OracleInfo,
555
+ totalCollateral: bigint,
556
+ ) {
557
+ switch (contractDescriptor.contractDescriptorType) {
558
+ case ContractDescriptorType.Enumerated: {
559
+ return this.GetPayoutsFromEnumeratedDescriptor(
560
+ dlcOffer,
561
+ contractDescriptor as EnumeratedDescriptor,
562
+ oracleInfo,
563
+ totalCollateral,
564
+ );
565
+ }
566
+ case ContractDescriptorType.NumericOutcome: {
567
+ const numericalDescriptor = contractDescriptor as NumericalDescriptor;
568
+ const payoutFunction = numericalDescriptor.payoutFunction;
569
+
570
+ // TODO: add a better check for this
571
+ const payoutCurvePiece =
572
+ payoutFunction.payoutFunctionPieces[0].payoutCurvePiece;
573
+
574
+ switch (payoutCurvePiece.payoutCurvePieceType) {
575
+ case PayoutCurvePieceType.Hyperbola:
576
+ return this.GetPayoutsFromPayoutFunction(
577
+ dlcOffer,
578
+ numericalDescriptor,
579
+ oracleInfo,
580
+ totalCollateral,
581
+ );
582
+ case PayoutCurvePieceType.Polynomial:
583
+ return this.GetPayoutsFromPolynomialPayoutFunction(
584
+ dlcOffer,
585
+ numericalDescriptor,
586
+ oracleInfo as SingleOracleInfo,
587
+ totalCollateral,
588
+ );
589
+ }
590
+ }
591
+ }
592
+ }
593
+
594
+ /**
595
+ * Converts a @node-dlc/bitcoin Tx to a DDK Transaction
596
+ * @param tx The @node-dlc/bitcoin transaction
597
+ * @returns DDK Transaction object
598
+ */
599
+ private convertTxToDdkTransaction(tx: Tx): DdkTransaction {
600
+ return {
601
+ version: tx.version,
602
+ lockTime: tx.locktime.value,
603
+ inputs: tx.inputs.map((input) => ({
604
+ txid: input.outpoint.txid.toString(),
605
+ vout: input.outpoint.outputIndex,
606
+ scriptSig: Buffer.from(''),
607
+ sequence: input.sequence.value,
608
+ witness: input.witness.map((witness) => witness.serialize()),
609
+ })),
610
+ outputs: tx.outputs.map((output) => ({
611
+ value: BigInt(output.value.sats),
612
+ scriptPubkey: output.scriptPubKey.serialize(),
613
+ })),
614
+ rawBytes: tx.serialize(),
615
+ };
616
+ }
617
+
618
+ public async createDlcTxs(
619
+ dlcOffer: DlcOffer,
620
+ dlcAccept: DlcAccept,
621
+ ): Promise<CreateDlcTxsResponse> {
622
+ const localFundPubkey = dlcOffer.fundingPubkey.toString('hex');
623
+ const remoteFundPubkey = dlcAccept.fundingPubkey.toString('hex');
624
+ const localFinalScriptPubkey = dlcOffer.payoutSpk.toString('hex');
625
+ const remoteFinalScriptPubkey = dlcAccept.payoutSpk.toString('hex');
626
+ const localChangeScriptPubkey = dlcOffer.changeSpk.toString('hex');
627
+ const remoteChangeScriptPubkey = dlcAccept.changeSpk.toString('hex');
628
+
629
+ // Separate regular inputs from DLC inputs (only from offeror side)
630
+ const localRegularInputs: Utxo[] = [];
631
+ const localDlcInputs: DdkDlcInputInfo[] = [];
632
+
633
+ for (const fundingInput of dlcOffer.fundingInputs) {
634
+ if (fundingInput.dlcInput) {
635
+ // This is a DLC input for splicing
636
+ // The pubkeys should be from the original DLC to correctly spend its funding output
637
+ localDlcInputs.push({
638
+ fundTx: this.convertTxToDdkTransaction(fundingInput.prevTx),
639
+ fundVout: fundingInput.prevTxVout,
640
+ localFundPubkey: fundingInput.dlcInput.localFundPubkey,
641
+ remoteFundPubkey: fundingInput.dlcInput.remoteFundPubkey,
642
+ fundAmount:
643
+ fundingInput.prevTx.outputs[fundingInput.prevTxVout].value.sats,
644
+ maxWitnessLen: fundingInput.maxWitnessLen,
645
+ inputSerialId: fundingInput.inputSerialId,
646
+ contractId: fundingInput.dlcInput.contractId,
647
+ });
648
+ } else {
649
+ // Regular input
650
+ const input = await this.fundingInputToInput(fundingInput, false);
651
+ localRegularInputs.push(input.toUtxo());
652
+ }
653
+ }
654
+
655
+ // Process remote inputs (no DLC inputs from acceptor side)
656
+ const remoteInputs: Utxo[] = await Promise.all(
657
+ dlcAccept.fundingInputs.map(async (fundingInput) => {
658
+ const input = await this.fundingInputToInput(fundingInput, false);
659
+ return input.toUtxo();
660
+ }),
661
+ );
662
+
663
+ // Calculate input amounts
664
+ const localInputAmount = localRegularInputs.reduce<number>(
665
+ (prev, cur) => prev + cur.amount.GetSatoshiAmount(),
666
+ 0,
667
+ );
668
+
669
+ const remoteInputAmount = remoteInputs.reduce<number>(
670
+ (prev, cur) => prev + cur.amount.GetSatoshiAmount(),
671
+ 0,
672
+ );
673
+
674
+ let payouts: PayoutRequest[] = [];
675
+ let messagesList: Messages[] = [];
676
+
677
+ if (
678
+ dlcOffer.contractInfo.type === MessageType.SingleContractInfo &&
679
+ (dlcOffer.contractInfo as SingleContractInfo).contractDescriptor.type ===
680
+ ContractDescriptorType.Enumerated
681
+ ) {
682
+ for (const outcome of (
683
+ (dlcOffer.contractInfo as SingleContractInfo)
684
+ .contractDescriptor as EnumeratedDescriptor
685
+ ).outcomes) {
686
+ payouts.push({
687
+ local: outcome.localPayout,
688
+ remote:
689
+ dlcOffer.offerCollateral +
690
+ dlcAccept.acceptCollateral -
691
+ outcome.localPayout,
692
+ });
693
+ messagesList.push({ messages: [outcome.outcome] });
694
+ }
695
+ } else {
696
+ const payoutResponses = this.GetPayouts(dlcOffer);
697
+ const { payouts: tempPayouts, messagesList: tempMessagesList } =
698
+ this.FlattenPayouts(payoutResponses);
699
+ payouts = tempPayouts;
700
+ messagesList = tempMessagesList;
701
+ }
702
+
703
+ const outcomes: Payout[] = payouts.map((payout) => ({
704
+ offer: BigInt(payout.local),
705
+ accept: BigInt(payout.remote),
706
+ }));
707
+
708
+ const localParams: PartyParams = {
709
+ fundPubkey: Buffer.from(localFundPubkey, 'hex'),
710
+ changeScriptPubkey: Buffer.from(localChangeScriptPubkey, 'hex'),
711
+ changeSerialId: BigInt(dlcOffer.changeSerialId),
712
+ payoutScriptPubkey: Buffer.from(localFinalScriptPubkey, 'hex'),
713
+ payoutSerialId: BigInt(dlcOffer.payoutSerialId),
714
+ inputs: localRegularInputs.map((input) => input.toTxInputInfo()),
715
+ inputAmount: BigInt(localInputAmount),
716
+ collateral: BigInt(dlcOffer.offerCollateral),
717
+ dlcInputs: localDlcInputs,
718
+ };
719
+
720
+ const remoteParams: PartyParams = {
721
+ fundPubkey: Buffer.from(remoteFundPubkey, 'hex'),
722
+ changeScriptPubkey: Buffer.from(remoteChangeScriptPubkey, 'hex'),
723
+ changeSerialId: BigInt(dlcAccept.changeSerialId),
724
+ payoutScriptPubkey: Buffer.from(remoteFinalScriptPubkey, 'hex'),
725
+ payoutSerialId: BigInt(dlcAccept.payoutSerialId),
726
+ inputs: remoteInputs.map((input) => input.toTxInputInfo()),
727
+ inputAmount: BigInt(remoteInputAmount),
728
+ collateral: BigInt(dlcAccept.acceptCollateral),
729
+ dlcInputs: [],
730
+ };
731
+
732
+ // Determine whether to use regular or spliced DLC transactions
733
+ const hasDlcInputs = localDlcInputs.length > 0;
734
+
735
+ let dlcTxs: DdkDlcTransactions;
736
+
737
+ if (hasDlcInputs) {
738
+ // Use spliced DLC transactions when DLC inputs are present
739
+ dlcTxs = await this._ddk.createSplicedDlcTransactions(
740
+ outcomes,
741
+ localParams,
742
+ remoteParams,
743
+ dlcOffer.refundLocktime,
744
+ BigInt(dlcOffer.feeRatePerVb),
745
+ 0,
746
+ dlcOffer.cetLocktime,
747
+ dlcOffer.fundOutputSerialId,
748
+ );
749
+ } else {
750
+ // Use regular DLC transactions when no DLC inputs
751
+ dlcTxs = this._ddk.createDlcTransactions(
752
+ outcomes,
753
+ localParams,
754
+ remoteParams,
755
+ dlcOffer.refundLocktime,
756
+ BigInt(dlcOffer.feeRatePerVb),
757
+ 0,
758
+ dlcOffer.cetLocktime,
759
+ BigInt(dlcOffer.fundOutputSerialId),
760
+ );
761
+ }
762
+
763
+ const dlcTransactions = new DlcTransactions();
764
+ dlcTransactions.fundTx = Tx.decode(
765
+ StreamReader.fromBuffer(dlcTxs.fund.rawBytes),
766
+ );
767
+
768
+ // Build serial IDs based on actual outputs in the transaction
769
+ const actualOutputs = dlcTransactions.fundTx.outputs;
770
+ const serialIds: bigint[] = [];
771
+
772
+ // Always include the funding output serial ID
773
+ serialIds.push(BigInt(dlcOffer.fundOutputSerialId));
774
+
775
+ // Only include change serial IDs if there are actually change outputs
776
+ // For exact amount DLCs with no change, there will be only 1 output (the funding output)
777
+ if (actualOutputs.length > 1) {
778
+ // Multiple outputs means there are change outputs
779
+ if (dlcOffer.offerCollateral > 0n) {
780
+ serialIds.push(BigInt(dlcOffer.changeSerialId));
781
+ }
782
+ if (dlcAccept.acceptCollateral > 0n) {
783
+ serialIds.push(BigInt(dlcAccept.changeSerialId));
784
+ }
785
+ }
786
+
787
+ dlcTransactions.fundTxVout = serialIds
788
+ .sort((a, b) => (a < b ? -1 : a > b ? 1 : 0))
789
+ .findIndex((i) => BigInt(i) === BigInt(dlcOffer.fundOutputSerialId));
790
+
791
+ // Validate that the calculated fundTxVout is valid
792
+ if (
793
+ dlcTransactions.fundTxVout < 0 ||
794
+ dlcTransactions.fundTxVout >= dlcTransactions.fundTx.outputs.length
795
+ ) {
796
+ throw new Error(
797
+ `Invalid fundTxVout calculation: calculated=${dlcTransactions.fundTxVout}, ` +
798
+ `fundTx.outputs.length=${dlcTransactions.fundTx.outputs.length}, ` +
799
+ `fundOutputSerialId=${dlcOffer.fundOutputSerialId}, ` +
800
+ `serialIds=[${serialIds.join(', ')}], ` +
801
+ `offerCollateral=${dlcOffer.offerCollateral}, ` +
802
+ `acceptCollateral=${dlcAccept.acceptCollateral}`,
803
+ );
804
+ }
805
+
806
+ dlcTransactions.cets = dlcTxs.cets.map((cetTx) =>
807
+ Tx.decode(StreamReader.fromBuffer(cetTx.rawBytes)),
808
+ );
809
+ dlcTransactions.refundTx = Tx.decode(
810
+ StreamReader.fromBuffer(dlcTxs.refund.rawBytes),
811
+ );
812
+
813
+ return { dlcTransactions, messagesList };
814
+ }
815
+
816
+ /**
817
+ * Computes DLC-spec compliant tagged attestation message digest
818
+ * This matches what the oracle should sign according to the DLC specification
819
+ */
820
+ private computeTaggedAttestationMessage(outcome: string): string {
821
+ // DLC spec: H(H("DLC/oracle/attestation/v0") || H("DLC/oracle/attestation/v0") || H(outcome))
822
+ const tag = Buffer.from('DLC/oracle/attestation/v0', 'utf8');
823
+ const tagHash = sha256(tag);
824
+ const outcomeBuffer = Buffer.from(outcome, 'utf8');
825
+
826
+ // Compute H(tagHash || tagHash || outcomeHash)
827
+ const message = sha256(Buffer.concat([tagHash, tagHash, outcomeBuffer]));
828
+ return message.toString('hex');
829
+ }
830
+
831
+ /**
832
+ * Convert message lists to the format expected by DDK FFI
833
+ * DDK expects 32-byte message digests (tagged attestation messages)
834
+ */
835
+ private convertMessagesForDdk(tempMessagesList: Messages[]): Buffer[][][] {
836
+ return tempMessagesList.map((message) => [
837
+ message.messages.map((m) =>
838
+ // Convert outcome string to tagged attestation message (32-byte hash)
839
+ Buffer.from(this.computeTaggedAttestationMessage(m), 'hex'),
840
+ ),
841
+ ]);
842
+ }
843
+
844
+ private GenerateEnumMessages(oracleEvent: OracleEvent): Messages[] {
845
+ const eventDescriptor = oracleEvent.eventDescriptor as EnumEventDescriptor;
846
+
847
+ // For enum events, each oracle has one nonce and can attest to one of the possible outcomes
848
+ const messagesList: Messages[] = [];
849
+
850
+ // Pass raw outcome strings to dlcdevkit - it will handle the tagged hashing internally
851
+ // dlcdevkit expects raw strings and calls tagged_attestation_msg() internally
852
+ const messages = eventDescriptor.outcomes;
853
+ messagesList.push({ messages });
854
+
855
+ return messagesList;
856
+ }
857
+
858
+ private convertToJsonSerializable(obj: any): any {
859
+ if (obj === null || obj === undefined) {
860
+ return obj;
861
+ }
862
+
863
+ if (typeof obj === 'bigint') {
864
+ return Number(obj);
865
+ }
866
+
867
+ if (Buffer.isBuffer(obj)) {
868
+ return obj.toString('hex');
869
+ }
870
+
871
+ if (Array.isArray(obj)) {
872
+ return obj.map((item) => this.convertToJsonSerializable(item));
873
+ }
874
+
875
+ if (typeof obj === 'object') {
876
+ const result: any = {};
877
+ for (const [key, value] of Object.entries(obj)) {
878
+ result[key] = this.convertToJsonSerializable(value);
879
+ }
880
+ return result;
881
+ }
882
+
883
+ return obj;
884
+ }
885
+
886
+ private GenerateDigitDecompositionMessages(
887
+ oracleEvent: OracleEvent,
888
+ ): Messages[] {
889
+ const oracleNonces = oracleEvent.oracleNonces;
890
+ const eventDescriptor =
891
+ oracleEvent.eventDescriptor as DigitDecompositionEventDescriptor;
892
+
893
+ const messagesList: Messages[] = [];
894
+ oracleNonces.forEach(() => {
895
+ const messages = [];
896
+ for (let i = 0; i < eventDescriptor.base; i++) {
897
+ const m = i.toString();
898
+ messages.push(m);
899
+ }
900
+ messagesList.push({ messages });
901
+ });
902
+
903
+ return messagesList;
904
+ }
905
+
906
+ private GenerateMessages(oracleInfo: OracleInfo): Messages[] {
907
+ // Handle both SingleOracleInfo and MultiOracleInfo using type property instead of instanceof
908
+ let oracleEvent: OracleEvent;
909
+
910
+ if (oracleInfo.type === MessageType.SingleOracleInfo) {
911
+ const singleOracleInfo = oracleInfo as SingleOracleInfo;
912
+ oracleEvent = singleOracleInfo.announcement.oracleEvent;
913
+ } else if (oracleInfo.type === MessageType.MultiOracleInfo) {
914
+ const multiOracleInfo = oracleInfo as MultiOracleInfo;
915
+ // For multi-oracle, use the first announcement for now
916
+ // TODO: This might need more sophisticated handling for multi-oracle scenarios
917
+ if (multiOracleInfo.announcements.length === 0) {
918
+ throw Error('MultiOracleInfo must have at least one announcement');
919
+ }
920
+ oracleEvent = multiOracleInfo.announcements[0].oracleEvent;
921
+ } else {
922
+ throw Error(
923
+ `OracleInfo must be SingleOracleInfo or MultiOracleInfo, got type: ${oracleInfo.type}`,
924
+ );
925
+ }
926
+
927
+ switch (oracleEvent.eventDescriptor.type) {
928
+ case MessageType.EnumEventDescriptor:
929
+ return this.GenerateEnumMessages(oracleEvent);
930
+ case MessageType.DigitDecompositionEventDescriptor:
931
+ return this.GenerateDigitDecompositionMessages(oracleEvent);
932
+ default:
933
+ throw Error('EventDescriptor must be Enum or DigitDecomposition');
934
+ }
935
+ }
936
+
937
+ private GetContractOraclePairs(
938
+ _contractInfo: ContractInfo,
939
+ ): { contractDescriptor: ContractDescriptor; oracleInfo: OracleInfo }[] {
940
+ // Use contractInfoType property instead of instanceof for more reliable type checking
941
+ if (_contractInfo.contractInfoType === ContractInfoType.Single) {
942
+ const singleInfo = _contractInfo as SingleContractInfo;
943
+ return [
944
+ {
945
+ contractDescriptor: singleInfo.contractDescriptor,
946
+ oracleInfo: singleInfo.oracleInfo,
947
+ },
948
+ ];
949
+ } else if (_contractInfo.contractInfoType === ContractInfoType.Disjoint) {
950
+ const disjointInfo = _contractInfo as DisjointContractInfo;
951
+ return disjointInfo.contractOraclePairs;
952
+ } else {
953
+ throw Error('ContractInfo must be Single or Disjoint');
954
+ }
955
+ }
956
+
957
+ private getFundOutputValueSats(dlcTxs: DlcTransactions): bigint {
958
+ const fundOutput = dlcTxs.fundTx.outputs[dlcTxs.fundTxVout];
959
+ if (!fundOutput || !fundOutput.value) {
960
+ throw new Error(
961
+ `Invalid fund output at vout ${dlcTxs.fundTxVout}: ` +
962
+ `outputs.length=${dlcTxs.fundTx.outputs.length}, ` +
963
+ `output exists=${!!fundOutput}, ` +
964
+ `output.value exists=${!!(fundOutput && fundOutput.value)}`,
965
+ );
966
+ }
967
+ return fundOutput.value.sats;
968
+ }
969
+
970
+ private async CreateCetAdaptorAndRefundSigs(
971
+ dlcOffer: DlcOffer,
972
+ dlcAccept: DlcAccept,
973
+ dlcTxs: DlcTransactions,
974
+ messagesList: Messages[],
975
+ isOfferer: boolean,
976
+ ): Promise<CreateCetAdaptorAndRefundSigsResponse> {
977
+ const network = await this.getConnectedNetwork();
978
+
979
+ const cetsHex = dlcTxs.cets.map((cet) => cet.serialize().toString('hex'));
980
+
981
+ // Create the correct P2WSH multisig funding script
982
+ const fundingPubKeys =
983
+ Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
984
+ ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
985
+ : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
986
+
987
+ const p2ms = payments.p2ms({
988
+ m: 2,
989
+ pubkeys: fundingPubKeys,
990
+ network,
991
+ });
992
+
993
+ // We need the redeem script (multisig), not the P2WSH output
994
+ const fundingSPK = p2ms.output!;
995
+
996
+ // For finding the private key, we still need the individual P2WPKH address
997
+ const individualFundingSPK = Script.p2wpkhLock(
998
+ hash160(isOfferer ? dlcOffer.fundingPubkey : dlcAccept.fundingPubkey),
999
+ )
1000
+ .serialize()
1001
+ .slice(1);
1002
+
1003
+ const fundingAddress: string = address.fromOutputScript(
1004
+ individualFundingSPK,
1005
+ network,
1006
+ );
1007
+
1008
+ const { derivationPath } = await this.client.wallet.findAddress([
1009
+ fundingAddress,
1010
+ ]);
1011
+
1012
+ const fundPrivateKeyPair = await this.getMethod('keyPair')(derivationPath);
1013
+ const fundPrivateKey = Buffer.from(fundPrivateKeyPair.__D).toString('hex');
1014
+
1015
+ const contractOraclePairs = this.GetContractOraclePairs(
1016
+ dlcOffer.contractInfo,
1017
+ );
1018
+
1019
+ const sigs: ISig[][] = [];
1020
+
1021
+ if (
1022
+ dlcOffer.contractInfo.contractInfoType === ContractInfoType.Single &&
1023
+ (dlcOffer.contractInfo as SingleContractInfo).contractDescriptor.type ===
1024
+ MessageType.ContractDescriptorV0
1025
+ ) {
1026
+ for (const { oracleInfo } of contractOraclePairs) {
1027
+ if (oracleInfo.type !== MessageType.SingleOracleInfo) {
1028
+ throw new Error('Only SingleOracleInfo supported in this context');
1029
+ }
1030
+ const oracleAnnouncement = (oracleInfo as SingleOracleInfo)
1031
+ .announcement;
1032
+
1033
+ const adaptorSigRequestPromises: Promise<AdaptorSignature[]>[] = [];
1034
+
1035
+ const tempMessagesList = messagesList;
1036
+ const tempCetsHex = cetsHex;
1037
+
1038
+ const ddkOracleInfo: DdkOracleInfo = {
1039
+ publicKey: oracleAnnouncement.oraclePubkey,
1040
+ nonces: oracleAnnouncement.oracleEvent.oracleNonces,
1041
+ };
1042
+
1043
+ adaptorSigRequestPromises.push(
1044
+ (async () => {
1045
+ const cetsForDdk = tempCetsHex.map((cetHex) =>
1046
+ this.convertTxToDdkTransaction(
1047
+ Tx.decode(StreamReader.fromHex(cetHex)),
1048
+ ),
1049
+ );
1050
+ const messagesForDdk = this.convertMessagesForDdk(tempMessagesList);
1051
+
1052
+ const response = this._ddk.createCetAdaptorSigsFromOracleInfo(
1053
+ cetsForDdk,
1054
+ [ddkOracleInfo],
1055
+ Buffer.from(fundPrivateKey, 'hex'),
1056
+ fundingSPK,
1057
+ this.getFundOutputValueSats(dlcTxs),
1058
+ messagesForDdk,
1059
+ );
1060
+ return response;
1061
+ })(),
1062
+ );
1063
+
1064
+ const adaptorPairs: AdaptorSignature[] = (
1065
+ await Promise.all(adaptorSigRequestPromises)
1066
+ ).flat();
1067
+
1068
+ sigs.push(
1069
+ adaptorPairs.map((adaptorPair) => {
1070
+ return {
1071
+ encryptedSig: adaptorPair.signature,
1072
+ dleqProof: adaptorPair.proof,
1073
+ };
1074
+ }),
1075
+ );
1076
+ }
1077
+ } else {
1078
+ const indices = this.GetIndicesFromPayouts(this.GetPayouts(dlcOffer));
1079
+
1080
+ for (const [index, { oracleInfo }] of contractOraclePairs.entries()) {
1081
+ if (oracleInfo.type !== MessageType.SingleOracleInfo) {
1082
+ throw new Error('Only SingleOracleInfo supported in this context');
1083
+ }
1084
+ const oracleAnnouncement = (oracleInfo as SingleOracleInfo)
1085
+ .announcement;
1086
+
1087
+ const startingIndex = indices[index].startingMessagesIndex,
1088
+ endingIndex = indices[index + 1].startingMessagesIndex;
1089
+
1090
+ const oracleEventMessagesList = messagesList.slice(
1091
+ startingIndex,
1092
+ endingIndex,
1093
+ );
1094
+ const oracleEventCetsHex = cetsHex.slice(startingIndex, endingIndex);
1095
+
1096
+ const chunk = 100;
1097
+ const adaptorSigRequestPromises: Promise<AdaptorSignature[]>[] = [];
1098
+
1099
+ for (let i = 0, j = oracleEventMessagesList.length; i < j; i += chunk) {
1100
+ const tempMessagesList = oracleEventMessagesList.slice(i, i + chunk);
1101
+ const tempCetsHex = oracleEventCetsHex.slice(i, i + chunk);
1102
+
1103
+ const ddkOracleInfo: DdkOracleInfo = {
1104
+ publicKey: oracleAnnouncement.oraclePubkey,
1105
+ nonces: oracleAnnouncement.oracleEvent.oracleNonces,
1106
+ };
1107
+
1108
+ const messagesForDdk = this.convertMessagesForDdk(tempMessagesList);
1109
+
1110
+ adaptorSigRequestPromises.push(
1111
+ (async () => {
1112
+ const response = this._ddk.createCetAdaptorSigsFromOracleInfo(
1113
+ tempCetsHex.map((cetHex) =>
1114
+ this.convertTxToDdkTransaction(
1115
+ Tx.decode(StreamReader.fromHex(cetHex)),
1116
+ ),
1117
+ ),
1118
+ [ddkOracleInfo],
1119
+ Buffer.from(fundPrivateKey, 'hex'),
1120
+ fundingSPK,
1121
+ this.getFundOutputValueSats(dlcTxs),
1122
+ messagesForDdk,
1123
+ );
1124
+ return response;
1125
+ })(),
1126
+ );
1127
+ }
1128
+
1129
+ const adaptorPairs: AdaptorSignature[] = (
1130
+ await Promise.all(adaptorSigRequestPromises)
1131
+ ).flat();
1132
+
1133
+ sigs.push(
1134
+ adaptorPairs.map((adaptorPair) => {
1135
+ return {
1136
+ encryptedSig: adaptorPair.signature,
1137
+ dleqProof: adaptorPair.proof,
1138
+ };
1139
+ }),
1140
+ );
1141
+ }
1142
+ }
1143
+
1144
+ const refundSignature = this._ddk.getRawFundingTransactionInputSignature(
1145
+ this.convertTxToDdkTransaction(dlcTxs.refundTx),
1146
+ Buffer.from(fundPrivateKey, 'hex'),
1147
+ dlcTxs.fundTx.txId.toString(),
1148
+ dlcTxs.fundTxVout,
1149
+ dlcOffer.contractInfo.totalCollateral,
1150
+ );
1151
+
1152
+ const cetSignatures = new CetAdaptorSignatures();
1153
+ cetSignatures.sigs = sigs.flat();
1154
+
1155
+ return { cetSignatures, refundSignature };
1156
+ }
1157
+
1158
+ private async VerifyCetAdaptorAndRefundSigs(
1159
+ dlcOffer: DlcOffer,
1160
+ dlcAccept: DlcAccept,
1161
+ dlcSign: DlcSign,
1162
+ dlcTxs: DlcTransactions,
1163
+ messagesList: Messages[],
1164
+ isOfferer: boolean,
1165
+ ): Promise<void> {
1166
+ const cetsHex = dlcTxs.cets.map((cet) => cet.serialize().toString('hex'));
1167
+
1168
+ const contractOraclePairs = this.GetContractOraclePairs(
1169
+ dlcOffer.contractInfo,
1170
+ );
1171
+
1172
+ if (
1173
+ dlcOffer.contractInfo.type === MessageType.SingleContractInfo &&
1174
+ (dlcOffer.contractInfo as SingleContractInfo).contractDescriptor.type ===
1175
+ MessageType.ContractDescriptorV0
1176
+ ) {
1177
+ for (const { oracleInfo } of contractOraclePairs) {
1178
+ if (oracleInfo.type !== MessageType.SingleOracleInfo) {
1179
+ throw new Error('Only SingleOracleInfo supported in this context');
1180
+ }
1181
+ const oracleAnnouncement = (oracleInfo as SingleOracleInfo)
1182
+ .announcement;
1183
+
1184
+ const oracleEventSigs = isOfferer
1185
+ ? dlcAccept.cetAdaptorSignatures.sigs
1186
+ : dlcSign.cetAdaptorSignatures.sigs;
1187
+
1188
+ const sigsValidity: Promise<boolean>[] = [];
1189
+
1190
+ const tempMessagesList = messagesList;
1191
+ const tempSigs = oracleEventSigs;
1192
+ const tempAdaptorPairs = tempSigs.map((sig) => {
1193
+ return {
1194
+ signature: sig.encryptedSig,
1195
+ proof: sig.dleqProof,
1196
+ };
1197
+ });
1198
+
1199
+ // Create the correct P2WSH multisig funding script for verification
1200
+ const network = await this.getConnectedNetwork();
1201
+ const verifyFundingPubKeys =
1202
+ Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
1203
+ ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
1204
+ : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
1205
+
1206
+ const verifyP2ms = payments.p2ms({
1207
+ m: 2,
1208
+ pubkeys: verifyFundingPubKeys,
1209
+ network,
1210
+ });
1211
+
1212
+ // We need the redeem script (multisig), not the P2WSH output
1213
+ const fundingSPK = verifyP2ms.output!;
1214
+
1215
+ const ddkOracleInfo: DdkOracleInfo = {
1216
+ publicKey: oracleAnnouncement.oraclePubkey,
1217
+ nonces: oracleAnnouncement.oracleEvent.oracleNonces,
1218
+ };
1219
+
1220
+ const pubkey = isOfferer
1221
+ ? dlcAccept.fundingPubkey
1222
+ : dlcOffer.fundingPubkey;
1223
+
1224
+ const messagesForDdk = this.convertMessagesForDdk(tempMessagesList);
1225
+
1226
+ sigsValidity.push(
1227
+ (async () => {
1228
+ const response = this._ddk.verifyCetAdaptorSigsFromOracleInfo(
1229
+ tempAdaptorPairs,
1230
+ dlcTxs.cets.map((cet) => this.convertTxToDdkTransaction(cet)),
1231
+ [ddkOracleInfo],
1232
+ pubkey,
1233
+ fundingSPK,
1234
+ this.getFundOutputValueSats(dlcTxs),
1235
+ messagesForDdk,
1236
+ );
1237
+ return response;
1238
+ })(),
1239
+ );
1240
+
1241
+ let areSigsValid = (await Promise.all(sigsValidity)).every((b) => b);
1242
+
1243
+ await this.VerifyRefundSignatureAlt(
1244
+ dlcOffer,
1245
+ dlcAccept,
1246
+ dlcSign,
1247
+ dlcTxs,
1248
+ isOfferer,
1249
+ );
1250
+
1251
+ if (!areSigsValid) {
1252
+ throw new Error('Invalid signatures received');
1253
+ }
1254
+ }
1255
+ } else {
1256
+ const chunk = 100;
1257
+
1258
+ const indices = this.GetIndicesFromPayouts(this.GetPayouts(dlcOffer));
1259
+
1260
+ for (const [index, { oracleInfo }] of contractOraclePairs.entries()) {
1261
+ if (oracleInfo.type !== MessageType.SingleOracleInfo) {
1262
+ throw new Error('Only SingleOracleInfo supported in this context');
1263
+ }
1264
+ const oracleAnnouncement = (oracleInfo as SingleOracleInfo)
1265
+ .announcement;
1266
+
1267
+ const startingIndex = indices[index].startingMessagesIndex,
1268
+ endingIndex = indices[index + 1].startingMessagesIndex;
1269
+
1270
+ const oracleEventMessagesList = messagesList.slice(
1271
+ startingIndex,
1272
+ endingIndex,
1273
+ );
1274
+ const oracleEventCetsHex = cetsHex.slice(startingIndex, endingIndex);
1275
+ const oracleEventSigs = (
1276
+ isOfferer
1277
+ ? dlcAccept.cetAdaptorSignatures.sigs
1278
+ : dlcSign.cetAdaptorSignatures.sigs
1279
+ ).slice(startingIndex, endingIndex);
1280
+
1281
+ const sigsValidity: Promise<boolean>[] = [];
1282
+
1283
+ for (let i = 0, j = oracleEventMessagesList.length; i < j; i += chunk) {
1284
+ const tempMessagesList = oracleEventMessagesList.slice(i, i + chunk);
1285
+ const tempCetsHex = oracleEventCetsHex.slice(i, i + chunk);
1286
+ const tempSigs = oracleEventSigs.slice(i, i + chunk);
1287
+ const tempAdaptorPairs = tempSigs.map((sig) => {
1288
+ return {
1289
+ signature: sig.encryptedSig,
1290
+ proof: sig.dleqProof,
1291
+ };
1292
+ });
1293
+
1294
+ // Create the correct P2WSH multisig funding script
1295
+ const network = await this.getConnectedNetwork();
1296
+ const nonEnumFundingPubKeys =
1297
+ Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) ===
1298
+ -1
1299
+ ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
1300
+ : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
1301
+
1302
+ const nonEnumP2ms = payments.p2ms({
1303
+ m: 2,
1304
+ pubkeys: nonEnumFundingPubKeys,
1305
+ network,
1306
+ });
1307
+
1308
+ // We need the redeem script (multisig), not the P2WSH output
1309
+ const fundingSPK = nonEnumP2ms.output!;
1310
+
1311
+ const ddkOracleInfo: DdkOracleInfo = {
1312
+ publicKey: oracleAnnouncement.oraclePubkey,
1313
+ nonces: oracleAnnouncement.oracleEvent.oracleNonces,
1314
+ };
1315
+
1316
+ const pubkey = isOfferer
1317
+ ? dlcAccept.fundingPubkey
1318
+ : dlcOffer.fundingPubkey;
1319
+
1320
+ const messagesForDdk = this.convertMessagesForDdk(tempMessagesList);
1321
+
1322
+ sigsValidity.push(
1323
+ (async () => {
1324
+ const response = this._ddk.verifyCetAdaptorSigsFromOracleInfo(
1325
+ tempAdaptorPairs,
1326
+ tempCetsHex.map((cet) =>
1327
+ this.convertTxToDdkTransaction(
1328
+ Tx.decode(StreamReader.fromHex(cet)),
1329
+ ),
1330
+ ),
1331
+ [ddkOracleInfo],
1332
+ pubkey,
1333
+ fundingSPK,
1334
+ this.getFundOutputValueSats(dlcTxs),
1335
+ messagesForDdk,
1336
+ );
1337
+ return response;
1338
+ })(),
1339
+ );
1340
+ }
1341
+
1342
+ let areSigsValid = (await Promise.all(sigsValidity)).every((b) => b);
1343
+
1344
+ // Verify refund signature using PSBT approach
1345
+ await this.VerifyRefundSignatureAlt(
1346
+ dlcOffer,
1347
+ dlcAccept,
1348
+ dlcSign,
1349
+ dlcTxs,
1350
+ isOfferer,
1351
+ );
1352
+
1353
+ if (!areSigsValid) {
1354
+ throw new Error('Invalid CET adaptor signatures received');
1355
+ }
1356
+ }
1357
+ }
1358
+ }
1359
+
1360
+ private async CreateFundingSigsAlt(
1361
+ dlcOffer: DlcOffer,
1362
+ dlcAccept: DlcAccept,
1363
+ dlcTxs: DlcTransactions,
1364
+ isOfferer: boolean,
1365
+ ): Promise<FundingSignatures> {
1366
+ const transaction = btTransaction.fromBuffer(dlcTxs.fundTx.serialize());
1367
+ const network = await this.getConnectedNetwork();
1368
+ const psbt = new Psbt({ network });
1369
+
1370
+ // Combine all funding inputs from both parties
1371
+ const allFundingInputs = [
1372
+ ...dlcOffer.fundingInputs,
1373
+ ...dlcAccept.fundingInputs,
1374
+ ];
1375
+
1376
+ // Sort by inputSerialId to reconstruct proper transaction order
1377
+ allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
1378
+
1379
+ // Add all inputs to PSBT with proper witnessUtxo
1380
+ for (const fundingInput of allFundingInputs) {
1381
+ const prevOut = fundingInput.prevTx.outputs[fundingInput.prevTxVout];
1382
+
1383
+ // Use the same pattern as existing code - slice(1) to remove length prefix
1384
+ const witnessUtxo = {
1385
+ script: prevOut.scriptPubKey.serialize().subarray(1),
1386
+ value: Number(prevOut.value.sats),
1387
+ };
1388
+
1389
+ // Use sequence from the original transaction to ensure consistency
1390
+ const originalInput = transaction.ins.find(
1391
+ (input) =>
1392
+ input.hash.reverse().toString('hex') ===
1393
+ fundingInput.prevTx.txId.toString() &&
1394
+ input.index === fundingInput.prevTxVout,
1395
+ );
1396
+ const sequenceValue = originalInput
1397
+ ? originalInput.sequence
1398
+ : Number(fundingInput.sequence);
1399
+
1400
+ psbt.addInput({
1401
+ hash: fundingInput.prevTx.txId.toString(),
1402
+ index: fundingInput.prevTxVout,
1403
+ sequence: sequenceValue,
1404
+ witnessUtxo,
1405
+ });
1406
+ }
1407
+
1408
+ // Add all outputs to PSBT (maintains transaction structure)
1409
+ for (const output of transaction.outs) {
1410
+ psbt.addOutput({
1411
+ address: address.fromOutputScript(output.script, network),
1412
+ value: output.value,
1413
+ });
1414
+ }
1415
+
1416
+ // Determine which inputs belong to this party
1417
+ const partyFundingInputs = isOfferer
1418
+ ? dlcOffer.fundingInputs
1419
+ : dlcAccept.fundingInputs;
1420
+
1421
+ // Convert party's funding inputs to Input objects and get private keys
1422
+ const partyInputs: Input[] = await Promise.all(
1423
+ partyFundingInputs.map(async (fundingInput) => {
1424
+ return this.fundingInputToInput(fundingInput);
1425
+ }),
1426
+ );
1427
+
1428
+ const inputPrivKeys = await this.GetPrivKeysForInputs(partyInputs);
1429
+
1430
+ // Create map of this party's inputs for efficient lookup
1431
+ const partyInputMap = new Map<string, { input: Input; privKey: string }>();
1432
+ partyInputs.forEach((input, index) => {
1433
+ const key = `${input.txid}:${input.vout}`;
1434
+ partyInputMap.set(key, { input, privKey: inputPrivKeys[index] });
1435
+ });
1436
+
1437
+ // Initialize witness elements array to match all inputs
1438
+ const witnessElements: ScriptWitnessV0[][] = [];
1439
+
1440
+ // Sign only this party's inputs
1441
+ for (
1442
+ let inputIndex = 0;
1443
+ inputIndex < allFundingInputs.length;
1444
+ inputIndex++
1445
+ ) {
1446
+ const fundingInput = allFundingInputs[inputIndex];
1447
+ const inputKey = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
1448
+
1449
+ if (partyInputMap.has(inputKey)) {
1450
+ // This input belongs to this party - sign it
1451
+ const { privKey } = partyInputMap.get(inputKey)!;
1452
+ const keyPair = ECPair.fromPrivateKey(Buffer.from(privKey, 'hex'));
1453
+
1454
+ // Check if this is a DLC input (2-of-2 multisig from previous DLC)
1455
+ if (fundingInput.dlcInput) {
1456
+ // For DLC inputs, we'll need to handle 2-of-2 multisig signing differently
1457
+ // For now, throw an error as this requires implementing multisig support
1458
+ throw new Error(
1459
+ 'DLC input signing not yet implemented in CreateFundingSigsAlt',
1460
+ );
1461
+ } else {
1462
+ // For P2WPKH inputs, use PSBT signing
1463
+ psbt.signInput(inputIndex, keyPair);
1464
+
1465
+ // Extract signature from partial signatures (more reliable than finalization)
1466
+ const inputData = psbt.data.inputs[inputIndex];
1467
+ const partialSigs = inputData.partialSig;
1468
+ if (!partialSigs || partialSigs.length === 0) {
1469
+ throw new Error(
1470
+ `No signatures found for input ${inputIndex} after signing`,
1471
+ );
1472
+ }
1473
+
1474
+ // For P2WPKH, create witness manually: [signature, publicKey]
1475
+ const sigWitness = new ScriptWitnessV0();
1476
+ sigWitness.witness = partialSigs[0].signature;
1477
+ const pubKeyWitness = new ScriptWitnessV0();
1478
+ pubKeyWitness.witness = keyPair.publicKey;
1479
+
1480
+ witnessElements.push([sigWitness, pubKeyWitness]);
1481
+ }
1482
+ }
1483
+ // Note: We don't add anything to witnessElements for other party's inputs
1484
+ }
1485
+
1486
+ const fundingSignatures = new FundingSignatures();
1487
+ fundingSignatures.witnessElements = witnessElements;
1488
+
1489
+ return fundingSignatures;
1490
+ }
1491
+
1492
+ private async VerifyFundingSigsAlt(
1493
+ dlcOffer: DlcOffer,
1494
+ dlcAccept: DlcAccept,
1495
+ dlcSign: DlcSign,
1496
+ dlcTxs: DlcTransactions,
1497
+ isOfferer: boolean,
1498
+ ): Promise<void> {
1499
+ const network = await this.getConnectedNetwork();
1500
+ const transaction = btTransaction.fromBuffer(dlcTxs.fundTx.serialize());
1501
+
1502
+ // Get the party whose signatures we're verifying
1503
+ // If we're the offerer, we verify accepter's signatures (from dlcSign)
1504
+ // If we're the accepter, we verify offerer's signatures (from dlcSign)
1505
+ const signingPartyInputs = isOfferer
1506
+ ? dlcAccept.fundingInputs
1507
+ : dlcOffer.fundingInputs;
1508
+
1509
+ // Combine all inputs to get the correct ordering
1510
+ const allFundingInputs = [
1511
+ ...dlcOffer.fundingInputs,
1512
+ ...dlcAccept.fundingInputs,
1513
+ ];
1514
+ allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
1515
+
1516
+ // Compare transaction IDs
1517
+ const dlcFundTxId = dlcTxs.fundTx.txId.toString();
1518
+ const psbtBuilderTxId = transaction.getId();
1519
+
1520
+ assert(dlcFundTxId === psbtBuilderTxId, 'Transaction IDs do not match');
1521
+
1522
+ // Create a PSBT for signature verification
1523
+ const psbt = new Psbt({ network });
1524
+
1525
+ // Add all inputs (needed for proper sighash calculation)
1526
+ for (const input of allFundingInputs) {
1527
+ const prevOutput = input.prevTx.outputs[input.prevTxVout];
1528
+ psbt.addInput({
1529
+ hash: input.prevTx.txId.toString(),
1530
+ index: input.prevTxVout,
1531
+ sequence: 0,
1532
+ witnessUtxo: {
1533
+ script: prevOutput.scriptPubKey.serialize().subarray(1),
1534
+ value: Number(prevOutput.value.sats),
1535
+ },
1536
+ });
1537
+ }
1538
+
1539
+ // Add all outputs
1540
+ for (const output of transaction.outs) {
1541
+ psbt.addOutput({
1542
+ address: address.fromOutputScript(output.script, network),
1543
+ value: output.value,
1544
+ });
1545
+ }
1546
+
1547
+ if (
1548
+ psbt.inputCount !== transaction.ins.length ||
1549
+ psbt.txOutputs.length !== transaction.outs.length
1550
+ ) {
1551
+ throw new Error(`PSBT structure doesn't match original transaction`);
1552
+ }
1553
+
1554
+ // Add the funding signatures to the PSBT as partial signatures
1555
+ let witnessIndex = 0;
1556
+ for (const fundingInput of signingPartyInputs) {
1557
+ // Skip DLC inputs for now (same as CreateFundingSigsAlt)
1558
+ if (fundingInput.dlcInput) {
1559
+ continue;
1560
+ }
1561
+
1562
+ if (witnessIndex >= dlcSign.fundingSignatures.witnessElements.length) {
1563
+ throw new Error(
1564
+ `Not enough witness elements: expected at least ${witnessIndex + 1}, got ${dlcSign.fundingSignatures.witnessElements.length}`,
1565
+ );
1566
+ }
1567
+
1568
+ const witnessElement =
1569
+ dlcSign.fundingSignatures.witnessElements[witnessIndex];
1570
+ const signature = witnessElement[0].witness;
1571
+ const publicKey = witnessElement[1].witness;
1572
+
1573
+ // Find this input's index in the sorted transaction
1574
+ const inputIndex = allFundingInputs.findIndex(
1575
+ (input) =>
1576
+ input.prevTx.txId.toString() ===
1577
+ fundingInput.prevTx.txId.toString() &&
1578
+ input.prevTxVout === fundingInput.prevTxVout,
1579
+ );
1580
+
1581
+ if (inputIndex === -1) {
1582
+ throw new Error(
1583
+ `Input not found in transaction: ${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`,
1584
+ );
1585
+ }
1586
+
1587
+ psbt.updateInput(inputIndex, {
1588
+ partialSig: [{ pubkey: publicKey, signature: signature }],
1589
+ });
1590
+
1591
+ psbt.validateSignaturesOfInput(
1592
+ inputIndex,
1593
+ (pubkey: Buffer, msghash: Buffer, signature: Buffer) => {
1594
+ return ecc.verify(msghash, pubkey, signature);
1595
+ },
1596
+ );
1597
+
1598
+ witnessIndex++;
1599
+ }
1600
+ }
1601
+
1602
+ private async VerifyRefundSignatureAlt(
1603
+ dlcOffer: DlcOffer,
1604
+ dlcAccept: DlcAccept,
1605
+ dlcSign: DlcSign,
1606
+ dlcTxs: DlcTransactions,
1607
+ isOfferer: boolean,
1608
+ ): Promise<void> {
1609
+ const network = await this.getConnectedNetwork();
1610
+
1611
+ // Get the refund signature we need to verify
1612
+ // If we're the offerer, we verify accepter's refund signature (from dlcAccept)
1613
+ // If we're the accepter, we verify offerer's refund signature (from dlcSign)
1614
+ const refundSignature = isOfferer
1615
+ ? dlcAccept.refundSignature
1616
+ : dlcSign.refundSignature;
1617
+ const signingPubkey = isOfferer
1618
+ ? dlcAccept.fundingPubkey
1619
+ : dlcOffer.fundingPubkey;
1620
+
1621
+ // Verify refund transaction locktime matches expected
1622
+ if (Number(dlcTxs.refundTx.locktime) !== dlcOffer.refundLocktime) {
1623
+ throw new Error(
1624
+ `Refund transaction locktime ${dlcTxs.refundTx.locktime} does not match expected ${dlcOffer.refundLocktime}`,
1625
+ );
1626
+ }
1627
+
1628
+ // Create a PSBT for the refund transaction verification
1629
+ const psbt = new Psbt({ network });
1630
+
1631
+ // Add the funding input
1632
+ const fundingOutput = dlcTxs.fundTx.outputs[dlcTxs.fundTxVout];
1633
+ psbt.addInput({
1634
+ hash: dlcTxs.fundTx.txId.toString(),
1635
+ index: dlcTxs.fundTxVout,
1636
+ sequence: 0,
1637
+ witnessUtxo: {
1638
+ script: fundingOutput.scriptPubKey.serialize().subarray(1), // Remove length prefix
1639
+ value: Number(fundingOutput.value.sats),
1640
+ },
1641
+ witnessScript: this.CreateFundingScript(dlcOffer, dlcAccept), // 2-of-2 multisig script
1642
+ });
1643
+
1644
+ // Add the refund output
1645
+ const refundOutput = dlcTxs.refundTx.outputs[0];
1646
+ psbt.addOutput({
1647
+ address: address.fromOutputScript(
1648
+ refundOutput.scriptPubKey.serialize().subarray(1),
1649
+ network,
1650
+ ),
1651
+ value: Number(refundOutput.value.sats),
1652
+ });
1653
+
1654
+ // Set the locktime to match the refund transaction
1655
+ psbt.setLocktime(Number(dlcTxs.refundTx.locktime));
1656
+
1657
+ // Add the refund signature as a partial signature
1658
+ psbt.updateInput(0, {
1659
+ partialSig: [{ pubkey: signingPubkey, signature: refundSignature }],
1660
+ });
1661
+
1662
+ // Validate the refund signature
1663
+ try {
1664
+ psbt.validateSignaturesOfInput(
1665
+ 0,
1666
+ (pubkey: Buffer, msghash: Buffer, signature: Buffer) => {
1667
+ return ecc.verify(msghash, pubkey, signature);
1668
+ },
1669
+ );
1670
+ } catch (error) {
1671
+ throw new Error(
1672
+ `Refund signature validation failed for ${isOfferer ? 'accepter' : 'offerer'}: ${error.message}`,
1673
+ );
1674
+ }
1675
+ }
1676
+
1677
+ private CreateFundingScript(
1678
+ dlcOffer: DlcOffer,
1679
+ dlcAccept: DlcAccept,
1680
+ ): Buffer {
1681
+ const network = this.getBitcoinJsNetwork();
1682
+
1683
+ // Sort funding pubkeys in lexicographical order as per DLC spec
1684
+ const fundingPubKeys =
1685
+ Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
1686
+ ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
1687
+ : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
1688
+
1689
+ // Create 2-of-2 multisig script
1690
+ const p2ms = payments.p2ms({
1691
+ m: 2,
1692
+ pubkeys: fundingPubKeys,
1693
+ network,
1694
+ });
1695
+
1696
+ const paymentVariant = payments.p2wsh({
1697
+ redeem: p2ms,
1698
+ network,
1699
+ });
1700
+
1701
+ return paymentVariant.redeem!.output!;
1702
+ }
1703
+
1704
+ private async CreateFundingTx(
1705
+ dlcOffer: DlcOffer,
1706
+ dlcAccept: DlcAccept,
1707
+ dlcSign: DlcSign,
1708
+ dlcTxs: DlcTransactions,
1709
+ fundingSignatures: FundingSignatures,
1710
+ ): Promise<Tx> {
1711
+ const network = await this.getConnectedNetwork();
1712
+ const psbt = new Psbt({ network });
1713
+
1714
+ // Combine and sort all funding inputs by serial ID (same as CreateFundingSigsAlt)
1715
+ const allFundingInputs = [
1716
+ ...dlcOffer.fundingInputs,
1717
+ ...dlcAccept.fundingInputs,
1718
+ ];
1719
+ allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
1720
+
1721
+ // Create a map of input txid:vout to witness elements
1722
+ const witnessMap = new Map<string, ScriptWitnessV0[]>();
1723
+
1724
+ // Map witness elements correctly - CreateFundingSigsAlt only creates witness elements
1725
+ // for the party's own inputs, so we need to map them correctly
1726
+
1727
+ // For dlcSign (offerer's signatures), map to offerer's inputs only
1728
+ let offererWitnessIndex = 0;
1729
+ dlcOffer.fundingInputs.forEach((fundingInput) => {
1730
+ // Skip DLC inputs for now as in CreateFundingSigsAlt
1731
+ if (fundingInput.dlcInput) {
1732
+ return;
1733
+ }
1734
+
1735
+ if (
1736
+ offererWitnessIndex < dlcSign.fundingSignatures.witnessElements.length
1737
+ ) {
1738
+ const key = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
1739
+ witnessMap.set(
1740
+ key,
1741
+ dlcSign.fundingSignatures.witnessElements[offererWitnessIndex],
1742
+ );
1743
+ offererWitnessIndex++;
1744
+ }
1745
+ });
1746
+
1747
+ // For fundingSignatures (accepter's signatures), map to accepter's inputs only
1748
+ let accepterWitnessIndex = 0;
1749
+ dlcAccept.fundingInputs.forEach((fundingInput) => {
1750
+ // Skip DLC inputs for now as in CreateFundingSigsAlt
1751
+ if (fundingInput.dlcInput) {
1752
+ return;
1753
+ }
1754
+
1755
+ if (accepterWitnessIndex < fundingSignatures.witnessElements.length) {
1756
+ const key = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
1757
+ witnessMap.set(
1758
+ key,
1759
+ fundingSignatures.witnessElements[accepterWitnessIndex],
1760
+ );
1761
+ accepterWitnessIndex++;
1762
+ }
1763
+ });
1764
+
1765
+ // Add all inputs to PSBT with proper sequence values
1766
+ const originalTransaction = btTransaction.fromBuffer(
1767
+ dlcTxs.fundTx.serialize(),
1768
+ );
1769
+
1770
+ for (const fundingInput of allFundingInputs) {
1771
+ const prevOut = fundingInput.prevTx.outputs[fundingInput.prevTxVout];
1772
+
1773
+ // Use same script handling as CreateFundingSigsAlt
1774
+ const witnessUtxo = {
1775
+ script: prevOut.scriptPubKey.serialize().subarray(1),
1776
+ value: Number(prevOut.value.sats),
1777
+ };
1778
+
1779
+ // Use sequence from the original transaction (same as CreateFundingSigsAlt)
1780
+ const originalInput = originalTransaction.ins.find(
1781
+ (input) =>
1782
+ input.hash.reverse().toString('hex') ===
1783
+ fundingInput.prevTx.txId.toString() &&
1784
+ input.index === fundingInput.prevTxVout,
1785
+ );
1786
+ const sequenceValue = originalInput
1787
+ ? originalInput.sequence
1788
+ : Number(fundingInput.sequence);
1789
+
1790
+ psbt.addInput({
1791
+ hash: fundingInput.prevTx.txId.toString(),
1792
+ index: fundingInput.prevTxVout,
1793
+ sequence: sequenceValue,
1794
+ witnessUtxo,
1795
+ });
1796
+ }
1797
+
1798
+ // Add all outputs from the funding transaction
1799
+ for (const output of originalTransaction.outs) {
1800
+ psbt.addOutput({
1801
+ address: address.fromOutputScript(output.script, network),
1802
+ value: output.value,
1803
+ });
1804
+ }
1805
+
1806
+ // Finalize inputs with their witness data
1807
+ for (
1808
+ let inputIndex = 0;
1809
+ inputIndex < allFundingInputs.length;
1810
+ inputIndex++
1811
+ ) {
1812
+ const fundingInput = allFundingInputs[inputIndex];
1813
+ const inputKey = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
1814
+
1815
+ const witnessElements = witnessMap.get(inputKey);
1816
+ if (witnessElements && witnessElements.length === 2) {
1817
+ // Skip DLC inputs for now as requested
1818
+ if (fundingInput.dlcInput) {
1819
+ continue;
1820
+ }
1821
+
1822
+ // For P2WPKH inputs, finalize the input with witness data
1823
+ const signature = witnessElements[0].witness;
1824
+ const publicKey = witnessElements[1].witness;
1825
+
1826
+ // Try a simpler approach - let bitcoinjs-lib handle witness construction
1827
+ psbt.finalizeInput(inputIndex, () => ({
1828
+ finalScriptSig: Buffer.alloc(0),
1829
+ finalScriptWitness: Buffer.concat([
1830
+ Buffer.from([0x02]), // witness stack count
1831
+ Buffer.from([signature.length]),
1832
+ signature,
1833
+ Buffer.from([publicKey.length]),
1834
+ publicKey,
1835
+ ]),
1836
+ }));
1837
+ }
1838
+ }
1839
+
1840
+ // Extract the final transaction
1841
+ const finalTx = psbt.extractTransaction();
1842
+
1843
+ // Convert back to the expected Tx format
1844
+ return Tx.decode(StreamReader.fromBuffer(finalTx.toBuffer()));
1845
+ }
1846
+
1847
+ async FindOutcomeIndexFromPolynomialPayoutCurvePiece(
1848
+ dlcOffer: DlcOffer,
1849
+ contractDescriptor: NumericalDescriptor,
1850
+ contractOraclePairIndex: number,
1851
+ polynomialPayoutCurvePiece: PolynomialPayoutCurvePiece,
1852
+ oracleAttestation: OracleAttestation,
1853
+ outcome: bigint,
1854
+ ): Promise<FindOutcomeResponse> {
1855
+ const polynomialCurve = PolynomialPayoutCurve.fromPayoutCurvePiece(
1856
+ polynomialPayoutCurvePiece,
1857
+ );
1858
+
1859
+ const payouts = polynomialPayoutCurvePiece.points.map((point) =>
1860
+ Number(point.outcomePayout),
1861
+ );
1862
+ const minPayout = Math.min(...payouts);
1863
+ const maxPayout = Math.max(...payouts);
1864
+
1865
+ const clampBN = (val: BigNumber) =>
1866
+ BigNumber.max(minPayout, BigNumber.min(val, maxPayout));
1867
+
1868
+ const payout = clampBN(polynomialCurve.getPayout(outcome));
1869
+
1870
+ const payoutResponses = this.GetPayouts(dlcOffer);
1871
+ const payoutIndexOffset =
1872
+ this.GetIndicesFromPayouts(payoutResponses)[contractOraclePairIndex]
1873
+ .startingMessagesIndex;
1874
+
1875
+ const { payoutGroups } = payoutResponses[contractOraclePairIndex];
1876
+
1877
+ const intervalsSorted = [
1878
+ ...contractDescriptor.roundingIntervals.intervals,
1879
+ ].sort((a, b) => Number(b.beginInterval) - Number(a.beginInterval));
1880
+
1881
+ const interval = intervalsSorted.find(
1882
+ (interval) => Number(outcome) >= Number(interval.beginInterval),
1883
+ );
1884
+
1885
+ const roundedPayout = BigInt(
1886
+ clampBN(
1887
+ new BigNumber(roundPayout(payout, interval.roundingMod).toString()),
1888
+ ).toString(),
1889
+ );
1890
+
1891
+ const outcomesFormatted = oracleAttestation.outcomes.map((outcome) =>
1892
+ parseInt(outcome),
1893
+ );
1894
+
1895
+ let index = 0;
1896
+ let groupIndex = -1;
1897
+ let groupLength = 0;
1898
+
1899
+ for (const payoutGroup of payoutGroups) {
1900
+ if (payoutGroup.payout === roundedPayout) {
1901
+ groupIndex = payoutGroup.groups.findIndex((group) => {
1902
+ return group.every((msg, i) => msg === outcomesFormatted[i]);
1903
+ });
1904
+ if (groupIndex === -1)
1905
+ throw Error(
1906
+ 'Failed to Find OutcomeIndex From PolynomialPayoutCurvePiece. \
1907
+ Payout Group found but incorrect group index',
1908
+ );
1909
+ index += groupIndex;
1910
+ groupLength = payoutGroup.groups[groupIndex].length;
1911
+ break;
1912
+ } else {
1913
+ index += payoutGroup.groups.length;
1914
+ }
1915
+ }
1916
+
1917
+ if (groupIndex === -1)
1918
+ throw Error(
1919
+ 'Failed to Find OutcomeIndex From PolynomialPayoutCurvePiece. \
1920
+ Payout Group not found',
1921
+ );
1922
+
1923
+ return { index: payoutIndexOffset + index, groupLength };
1924
+ }
1925
+
1926
+ async FindOutcomeIndexFromHyperbolaPayoutCurvePiece(
1927
+ _dlcOffer: DlcOffer,
1928
+ contractDescriptor: NumericalDescriptor,
1929
+ contractOraclePairIndex: number,
1930
+ hyperbolaPayoutCurvePiece: HyperbolaPayoutCurvePiece,
1931
+ oracleAttestation: OracleAttestation,
1932
+ outcome: bigint,
1933
+ ): Promise<FindOutcomeResponse> {
1934
+ const { dlcOffer } = checkTypes({ _dlcOffer });
1935
+
1936
+ const hyperbolaCurve = HyperbolaPayoutCurve.fromPayoutCurvePiece(
1937
+ hyperbolaPayoutCurvePiece,
1938
+ );
1939
+
1940
+ const clampBN = (val: BigNumber) =>
1941
+ BigNumber.max(
1942
+ 0,
1943
+ BigNumber.min(val, dlcOffer.contractInfo.totalCollateral.toString()),
1944
+ );
1945
+
1946
+ const payout = clampBN(hyperbolaCurve.getPayout(outcome));
1947
+
1948
+ const payoutResponses = this.GetPayouts(dlcOffer);
1949
+ const payoutIndexOffset =
1950
+ this.GetIndicesFromPayouts(payoutResponses)[contractOraclePairIndex]
1951
+ .startingMessagesIndex;
1952
+
1953
+ const { payoutGroups } = payoutResponses[contractOraclePairIndex];
1954
+
1955
+ const intervalsSorted = [
1956
+ ...contractDescriptor.roundingIntervals.intervals,
1957
+ ].sort((a, b) => Number(b.beginInterval) - Number(a.beginInterval));
1958
+
1959
+ const interval = intervalsSorted.find(
1960
+ (interval) => Number(outcome) >= Number(interval.beginInterval),
1961
+ );
1962
+
1963
+ const roundedPayout = BigInt(
1964
+ clampBN(
1965
+ new BigNumber(roundPayout(payout, interval.roundingMod).toString()),
1966
+ ).toString(),
1967
+ );
1968
+
1969
+ const outcomesFormatted = oracleAttestation.outcomes.map((outcome) =>
1970
+ parseInt(outcome),
1971
+ );
1972
+
1973
+ let index = 0;
1974
+ let groupIndex = -1;
1975
+ let groupLength = 0;
1976
+
1977
+ for (const [i, payoutGroup] of payoutGroups.entries()) {
1978
+ if (payoutGroup.payout === roundedPayout) {
1979
+ groupIndex = payoutGroup.groups.findIndex((group) => {
1980
+ return group.every((msg, i) => msg === outcomesFormatted[i]);
1981
+ });
1982
+ if (groupIndex !== -1) {
1983
+ index += groupIndex;
1984
+ groupLength = payoutGroup.groups[groupIndex].length;
1985
+ break;
1986
+ }
1987
+ } else if (
1988
+ payoutGroup.payout === BigInt(Math.round(Number(payout.toString()))) &&
1989
+ i !== 0
1990
+ ) {
1991
+ // Edge case to account for case where payout is maximum payout for DLC
1992
+ // But rounded payout does not round down
1993
+ if (payoutGroups[i - 1].payout === roundedPayout) {
1994
+ // Ensure that the previous payout group causes index to be incremented
1995
+ index += payoutGroups[i - 1].groups.length;
1996
+ }
1997
+
1998
+ groupIndex = payoutGroup.groups.findIndex((group) => {
1999
+ return group.every((msg, i) => msg === outcomesFormatted[i]);
2000
+ });
2001
+ if (groupIndex !== -1) {
2002
+ index += groupIndex;
2003
+ groupLength = payoutGroup.groups[groupIndex].length;
2004
+ break;
2005
+ }
2006
+ } else {
2007
+ index += payoutGroup.groups.length;
2008
+ }
2009
+ }
2010
+
2011
+ if (groupIndex === -1) {
2012
+ // Fallback to brute force search if payout-based search fails
2013
+ index = 0;
2014
+ groupLength = 0;
2015
+
2016
+ for (const [, payoutGroup] of payoutGroups.entries()) {
2017
+ groupIndex = payoutGroup.groups.findIndex((group) => {
2018
+ return group.every((msg, j) => msg === outcomesFormatted[j]);
2019
+ });
2020
+
2021
+ if (groupIndex !== -1) {
2022
+ index += groupIndex;
2023
+ groupLength = payoutGroup.groups[groupIndex].length;
2024
+ break;
2025
+ } else {
2026
+ index += payoutGroup.groups.length;
2027
+ }
2028
+ }
2029
+
2030
+ if (groupIndex === -1) {
2031
+ throw Error(
2032
+ 'Failed to Find OutcomeIndex From HyperbolaPayoutCurvePiece. \
2033
+ Payout Group not found even with brute force search',
2034
+ );
2035
+ }
2036
+ }
2037
+
2038
+ return { index: payoutIndexOffset + index, groupLength };
2039
+ }
2040
+
2041
+ async FindOutcomeIndex(
2042
+ dlcOffer: DlcOffer,
2043
+ oracleAttestation: OracleAttestation,
2044
+ ): Promise<FindOutcomeResponse> {
2045
+ const contractOraclePairs = this.GetContractOraclePairs(
2046
+ dlcOffer.contractInfo,
2047
+ );
2048
+ const contractOraclePairIndex = contractOraclePairs.findIndex(
2049
+ ({ oracleInfo }) => {
2050
+ if (oracleInfo.type !== MessageType.SingleOracleInfo) return false;
2051
+ const singleOracleInfo = oracleInfo as SingleOracleInfo;
2052
+ return (
2053
+ singleOracleInfo.announcement.oracleEvent.eventId ===
2054
+ oracleAttestation.eventId
2055
+ );
2056
+ },
2057
+ );
2058
+ assert(
2059
+ contractOraclePairIndex !== -1,
2060
+ 'OracleAttestation must be for an existing OracleEvent',
2061
+ );
2062
+
2063
+ const contractOraclePair = contractOraclePairs[contractOraclePairIndex];
2064
+
2065
+ const { contractDescriptor: _contractDescriptor, oracleInfo } =
2066
+ contractOraclePair;
2067
+ assert(
2068
+ _contractDescriptor.contractDescriptorType ===
2069
+ ContractDescriptorType.NumericOutcome,
2070
+ 'ContractDescriptor must be NumericOutcome',
2071
+ );
2072
+ const contractDescriptor = _contractDescriptor as NumericalDescriptor;
2073
+ const _payoutFunction = contractDescriptor.payoutFunction;
2074
+ assert(
2075
+ _payoutFunction.type === MessageType.PayoutFunction,
2076
+ 'PayoutFunction must be V0',
2077
+ );
2078
+
2079
+ if (oracleInfo.type !== MessageType.SingleOracleInfo) {
2080
+ throw new Error('Only SingleOracleInfo supported in this context');
2081
+ }
2082
+
2083
+ const singleOracleInfo = oracleInfo as SingleOracleInfo;
2084
+ const eventDescriptor = singleOracleInfo.announcement.oracleEvent
2085
+ .eventDescriptor as DigitDecompositionEventDescriptor;
2086
+ const payoutFunction = _payoutFunction as PayoutFunctionV0;
2087
+
2088
+ const base = eventDescriptor.base;
2089
+ const outcome: number = [...oracleAttestation.outcomes]
2090
+ .reverse()
2091
+ .reduce((acc, val, i) => acc + Number(val) * base ** i, 0);
2092
+
2093
+ const piecesSorted = payoutFunction.payoutFunctionPieces.sort(
2094
+ (a, b) =>
2095
+ Number(a.endPoint.eventOutcome) - Number(b.endPoint.eventOutcome),
2096
+ );
2097
+
2098
+ const piece = piecesSorted.find(
2099
+ (piece) => outcome < piece.endPoint.eventOutcome,
2100
+ );
2101
+
2102
+ switch (piece.payoutCurvePiece.type) {
2103
+ case MessageType.PolynomialPayoutCurvePiece:
2104
+ return this.FindOutcomeIndexFromPolynomialPayoutCurvePiece(
2105
+ dlcOffer,
2106
+ contractDescriptor,
2107
+ contractOraclePairIndex,
2108
+ piece.payoutCurvePiece as PolynomialPayoutCurvePiece,
2109
+ oracleAttestation,
2110
+ BigInt(outcome),
2111
+ );
2112
+ case MessageType.HyperbolaPayoutCurvePiece:
2113
+ return this.FindOutcomeIndexFromHyperbolaPayoutCurvePiece(
2114
+ dlcOffer,
2115
+ contractDescriptor,
2116
+ contractOraclePairIndex,
2117
+ piece.payoutCurvePiece as HyperbolaPayoutCurvePiece,
2118
+ oracleAttestation,
2119
+ BigInt(outcome),
2120
+ );
2121
+ case MessageType.OldHyperbolaPayoutCurvePiece:
2122
+ return this.FindOutcomeIndexFromHyperbolaPayoutCurvePiece(
2123
+ dlcOffer,
2124
+ contractDescriptor,
2125
+ contractOraclePairIndex,
2126
+ piece.payoutCurvePiece as HyperbolaPayoutCurvePiece,
2127
+ oracleAttestation,
2128
+ BigInt(outcome),
2129
+ );
2130
+ default:
2131
+ throw Error('Must be Hyperbola or Polynomial curve piece');
2132
+ }
2133
+ }
2134
+
2135
+ ValidateEvent(
2136
+ dlcOffer: DlcOffer,
2137
+ oracleAttestation: OracleAttestation,
2138
+ ): void {
2139
+ switch (dlcOffer.contractInfo.contractInfoType) {
2140
+ case ContractInfoType.Single: {
2141
+ const contractInfo = dlcOffer.contractInfo as SingleContractInfo;
2142
+ switch (contractInfo.contractDescriptor.contractDescriptorType) {
2143
+ case ContractDescriptorType.Enumerated: {
2144
+ const oracleInfo = contractInfo.oracleInfo;
2145
+ if (oracleInfo.type !== MessageType.SingleOracleInfo) {
2146
+ throw Error('Only SingleOracleInfo supported in this context');
2147
+ }
2148
+ const singleOracleInfo = oracleInfo as SingleOracleInfo;
2149
+ if (
2150
+ singleOracleInfo.announcement.oracleEvent.eventId !==
2151
+ oracleAttestation.eventId
2152
+ )
2153
+ throw Error('Incorrect Oracle Attestation. Event Id must match.');
2154
+ break;
2155
+ }
2156
+ case ContractDescriptorType.NumericOutcome: {
2157
+ const oracleInfo = contractInfo.oracleInfo;
2158
+ if (oracleInfo.type !== MessageType.SingleOracleInfo) {
2159
+ throw Error('Only SingleOracleInfo supported in this context');
2160
+ }
2161
+ const singleOracleInfo = oracleInfo as SingleOracleInfo;
2162
+ if (
2163
+ singleOracleInfo.announcement.oracleEvent.eventId !==
2164
+ oracleAttestation.eventId
2165
+ )
2166
+ throw Error('Incorrect Oracle Attestation. Event Id must match.');
2167
+ break;
2168
+ }
2169
+ default:
2170
+ throw Error('ConractDescriptor must be V0 or V1');
2171
+ }
2172
+ break;
2173
+ }
2174
+ case ContractInfoType.Disjoint: {
2175
+ const contractInfo = dlcOffer.contractInfo as DisjointContractInfo;
2176
+ const attestedOracleEvent = contractInfo.contractOraclePairs.find(
2177
+ ({ oracleInfo }) => {
2178
+ if (oracleInfo.type !== MessageType.SingleOracleInfo) return false;
2179
+ const singleOracleInfo = oracleInfo as SingleOracleInfo;
2180
+ return (
2181
+ singleOracleInfo.announcement.oracleEvent.eventId ===
2182
+ oracleAttestation.eventId
2183
+ );
2184
+ },
2185
+ );
2186
+
2187
+ if (!attestedOracleEvent)
2188
+ throw Error('Oracle event of attestation not found.');
2189
+
2190
+ break;
2191
+ }
2192
+ default:
2193
+ throw Error('ContractInfo must be V0 or V1');
2194
+ }
2195
+ }
2196
+
2197
+ async FindAndSignCet(
2198
+ dlcOffer: DlcOffer,
2199
+ dlcAccept: DlcAccept,
2200
+ dlcSign: DlcSign,
2201
+ dlcTxs: DlcTransactions,
2202
+ oracleAttestation: OracleAttestation,
2203
+ isOfferer?: boolean,
2204
+ ): Promise<Tx> {
2205
+ if (isOfferer === undefined)
2206
+ isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
2207
+
2208
+ const fundPrivateKey = await this.GetFundPrivateKey(
2209
+ dlcOffer,
2210
+ dlcAccept,
2211
+ isOfferer,
2212
+ );
2213
+
2214
+ let finalCet: string;
2215
+
2216
+ if (
2217
+ dlcOffer.contractInfo.contractInfoType === ContractInfoType.Single &&
2218
+ (dlcOffer.contractInfo as SingleContractInfo).contractDescriptor
2219
+ .contractDescriptorType === ContractDescriptorType.Enumerated
2220
+ ) {
2221
+ const contractDescriptor = (dlcOffer.contractInfo as SingleContractInfo)
2222
+ .contractDescriptor as EnumeratedDescriptor;
2223
+
2224
+ // Handle different contract descriptor outcome formats
2225
+ const attestedOutcome = oracleAttestation.outcomes[0];
2226
+
2227
+ const outcomeIndex = contractDescriptor.outcomes.findIndex((outcome) => {
2228
+ // Try direct string match first (for DDK2 test: '1', '2', '3')
2229
+ if (outcome.outcome === attestedOutcome) {
2230
+ return true;
2231
+ }
2232
+
2233
+ // Try sha256 hash match (for other tests with hashed outcomes)
2234
+ const attestedOutcomeHash = sha256(
2235
+ Buffer.from(attestedOutcome, 'utf8'),
2236
+ ).toString('hex');
2237
+ return outcome.outcome === attestedOutcomeHash;
2238
+ });
2239
+
2240
+ finalCet = this._ddk
2241
+ .signCet(
2242
+ this.convertTxToDdkTransaction(dlcTxs.cets[outcomeIndex]),
2243
+ isOfferer
2244
+ ? dlcAccept.cetAdaptorSignatures.sigs[outcomeIndex].encryptedSig
2245
+ : dlcSign.cetAdaptorSignatures.sigs[outcomeIndex].encryptedSig,
2246
+ oracleAttestation.signatures,
2247
+ Buffer.from(fundPrivateKey, 'hex'),
2248
+ isOfferer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey,
2249
+ isOfferer ? dlcOffer.fundingPubkey : dlcAccept.fundingPubkey,
2250
+ this.getFundOutputValueSats(dlcTxs),
2251
+ )
2252
+ .rawBytes.toString('hex');
2253
+ } else {
2254
+ const { index: outcomeIndex, groupLength } = await this.FindOutcomeIndex(
2255
+ dlcOffer,
2256
+ oracleAttestation,
2257
+ );
2258
+
2259
+ const sliceIndex = -(oracleAttestation.signatures.length - groupLength);
2260
+
2261
+ const oracleSignatures =
2262
+ sliceIndex === 0
2263
+ ? oracleAttestation.signatures
2264
+ : oracleAttestation.signatures.slice(0, sliceIndex);
2265
+
2266
+ finalCet = this._ddk
2267
+ .signCet(
2268
+ this.convertTxToDdkTransaction(dlcTxs.cets[outcomeIndex]),
2269
+ isOfferer
2270
+ ? dlcAccept.cetAdaptorSignatures.sigs[outcomeIndex].encryptedSig
2271
+ : dlcSign.cetAdaptorSignatures.sigs[outcomeIndex].encryptedSig,
2272
+ oracleSignatures,
2273
+ Buffer.from(fundPrivateKey, 'hex'),
2274
+ isOfferer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey,
2275
+ isOfferer ? dlcOffer.fundingPubkey : dlcAccept.fundingPubkey,
2276
+ this.getFundOutputValueSats(dlcTxs),
2277
+ )
2278
+ .rawBytes.toString('hex');
2279
+ }
2280
+
2281
+ // const finalCet = (await this.SignCet(signCetRequest)).hex;
2282
+
2283
+ return Tx.decode(StreamReader.fromHex(finalCet));
2284
+ }
2285
+
2286
+ private async GetFundAddress(
2287
+ dlcOffer: DlcOffer,
2288
+ dlcAccept: DlcAccept,
2289
+ isOfferer: boolean,
2290
+ ): Promise<string> {
2291
+ const network = await this.getConnectedNetwork();
2292
+
2293
+ const fundingSPK = Script.p2wpkhLock(
2294
+ hash160(isOfferer ? dlcOffer.fundingPubkey : dlcAccept.fundingPubkey),
2295
+ )
2296
+ .serialize()
2297
+ .slice(1);
2298
+
2299
+ const fundingAddress: string = address.fromOutputScript(
2300
+ fundingSPK,
2301
+ network,
2302
+ );
2303
+
2304
+ return fundingAddress;
2305
+ }
2306
+
2307
+ private async GetFundKeyPair(
2308
+ dlcOffer: DlcOffer,
2309
+ dlcAccept: DlcAccept,
2310
+ isOfferer: boolean,
2311
+ ): Promise<ECPairInterface> {
2312
+ const fundingAddress = await this.GetFundAddress(
2313
+ dlcOffer,
2314
+ dlcAccept,
2315
+ isOfferer,
2316
+ );
2317
+
2318
+ const { derivationPath } =
2319
+ await this.getMethod('getWalletAddress')(fundingAddress);
2320
+ const keyPair: ECPairInterface =
2321
+ await this.getMethod('keyPair')(derivationPath);
2322
+
2323
+ return keyPair;
2324
+ }
2325
+
2326
+ private async GetFundPrivateKey(
2327
+ dlcOffer: DlcOffer,
2328
+ dlcAccept: DlcAccept,
2329
+ isOfferer: boolean,
2330
+ ): Promise<string> {
2331
+ const fundPrivateKeyPair: ECPairInterface = await this.GetFundKeyPair(
2332
+ dlcOffer,
2333
+ dlcAccept,
2334
+ isOfferer,
2335
+ );
2336
+
2337
+ return Buffer.from(fundPrivateKeyPair.privateKey).toString('hex');
2338
+ }
2339
+
2340
+ async CreateCloseRawTxs(
2341
+ dlcOffer: DlcOffer,
2342
+ dlcAccept: DlcAccept,
2343
+ dlcTxs: DlcTransactions,
2344
+ closeInputAmount: bigint,
2345
+ isOfferer: boolean,
2346
+ _dlcCloses: DlcClose[] = [],
2347
+ fundingInputs?: FundingInput[],
2348
+ initiatorPayouts?: bigint[],
2349
+ ): Promise<string[]> {
2350
+ const network = await this.getConnectedNetwork();
2351
+
2352
+ let finalizer: DualClosingTxFinalizer;
2353
+ if (_dlcCloses.length === 0) {
2354
+ finalizer = new DualClosingTxFinalizer(
2355
+ fundingInputs,
2356
+ dlcOffer.payoutSpk,
2357
+ dlcAccept.payoutSpk,
2358
+ dlcOffer.feeRatePerVb,
2359
+ );
2360
+ }
2361
+
2362
+ const rawTransactionRequestPromises: Promise<string>[] = [];
2363
+ const rawCloseTxs = [];
2364
+
2365
+ const numPayouts =
2366
+ _dlcCloses.length === 0 ? initiatorPayouts.length : _dlcCloses.length;
2367
+
2368
+ for (let i = 0; i < numPayouts; i++) {
2369
+ let offerPayoutValue = BigInt(0);
2370
+ let acceptPayoutValue = BigInt(0);
2371
+
2372
+ if (_dlcCloses.length === 0) {
2373
+ const payout = initiatorPayouts[i];
2374
+ const payoutMinusOfferFees =
2375
+ finalizer.offerInitiatorFees > payout
2376
+ ? BigInt(0)
2377
+ : payout - finalizer.offerInitiatorFees;
2378
+ const collateralMinusPayout =
2379
+ payout > dlcOffer.contractInfo.totalCollateral
2380
+ ? BigInt(0)
2381
+ : dlcOffer.contractInfo.totalCollateral - payout;
2382
+
2383
+ offerPayoutValue = isOfferer
2384
+ ? closeInputAmount + payoutMinusOfferFees
2385
+ : collateralMinusPayout;
2386
+
2387
+ acceptPayoutValue = isOfferer
2388
+ ? collateralMinusPayout
2389
+ : closeInputAmount + payoutMinusOfferFees;
2390
+ } else {
2391
+ const dlcClose = checkTypes({ _dlcClose: _dlcCloses[i] }).dlcClose;
2392
+
2393
+ offerPayoutValue = dlcClose.offerPayoutSatoshis;
2394
+ acceptPayoutValue = dlcClose.acceptPayoutSatoshis;
2395
+ }
2396
+
2397
+ const txOuts = [];
2398
+
2399
+ if (Number(offerPayoutValue) > 0) {
2400
+ txOuts.push({
2401
+ address: address.fromOutputScript(dlcOffer.payoutSpk, network),
2402
+ amount: Number(offerPayoutValue),
2403
+ });
2404
+ }
2405
+
2406
+ if (Number(acceptPayoutValue) > 0) {
2407
+ txOuts.push({
2408
+ address: address.fromOutputScript(dlcAccept.payoutSpk, network),
2409
+ amount: Number(acceptPayoutValue),
2410
+ });
2411
+ }
2412
+
2413
+ if (dlcOffer.payoutSerialId > dlcAccept.payoutSerialId) txOuts.reverse();
2414
+
2415
+ const rawTransactionRequest: CreateRawTransactionRequest = {
2416
+ version: 2,
2417
+ locktime: 0,
2418
+ txins: [
2419
+ {
2420
+ txid: dlcTxs.fundTx.txId.serialize().reverse().toString('hex'),
2421
+ vout: dlcTxs.fundTxVout,
2422
+ sequence: 0,
2423
+ },
2424
+ ],
2425
+ txouts: txOuts,
2426
+ };
2427
+
2428
+ rawTransactionRequestPromises.push(
2429
+ (async () => {
2430
+ const response = await this.getMethod('CreateRawTransaction')(
2431
+ rawTransactionRequest,
2432
+ );
2433
+ return response.hex;
2434
+ })(),
2435
+ );
2436
+ }
2437
+
2438
+ const hexs: string[] = await Promise.all(rawTransactionRequestPromises);
2439
+
2440
+ rawCloseTxs.push(hexs);
2441
+
2442
+ return rawCloseTxs.flat();
2443
+ }
2444
+
2445
+ async CreateSignatureHashes(
2446
+ _dlcOffer: DlcOffer,
2447
+ _dlcAccept: DlcAccept,
2448
+ _dlcTxs: DlcTransactions,
2449
+ rawCloseTxs: string[],
2450
+ ): Promise<string[]> {
2451
+ const { dlcOffer, dlcAccept, dlcTxs } = checkTypes({
2452
+ _dlcOffer,
2453
+ _dlcAccept,
2454
+ _dlcTxs,
2455
+ });
2456
+
2457
+ const network = await this.getConnectedNetwork();
2458
+
2459
+ const fundingPubKeys =
2460
+ Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
2461
+ ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
2462
+ : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
2463
+
2464
+ const p2ms = payments.p2ms({
2465
+ m: 2,
2466
+ pubkeys: fundingPubKeys,
2467
+ network,
2468
+ });
2469
+
2470
+ const paymentVariant = payments.p2wsh({
2471
+ redeem: p2ms,
2472
+ network,
2473
+ });
2474
+
2475
+ const sigHashRequestPromises: Promise<string>[] = [];
2476
+ const sigHashes = [];
2477
+
2478
+ for (let i = 0; i < rawCloseTxs.length; i++) {
2479
+ const rawTx = rawCloseTxs[i];
2480
+
2481
+ const sigHashRequest: CreateSignatureHashRequest = {
2482
+ tx: rawTx,
2483
+ txin: {
2484
+ txid: dlcTxs.fundTx.txId.serialize().reverse().toString('hex'),
2485
+ vout: dlcTxs.fundTxVout,
2486
+ keyData: {
2487
+ hex: paymentVariant.redeem.output.toString('hex'),
2488
+ type: 'redeem_script',
2489
+ },
2490
+ amount: Number(this.getFundOutputValueSats(dlcTxs)),
2491
+ hashType: 'p2wsh',
2492
+ sighashType: 'all',
2493
+ sighashAnyoneCanPay: false,
2494
+ },
2495
+ };
2496
+
2497
+ sigHashRequestPromises.push(
2498
+ (async () => {
2499
+ const response = await this.getMethod('CreateSignatureHash')(
2500
+ sigHashRequest,
2501
+ );
2502
+ return response.sighash;
2503
+ })(),
2504
+ );
2505
+ }
2506
+
2507
+ const sighashes: string[] = await Promise.all(sigHashRequestPromises);
2508
+
2509
+ sigHashes.push(sighashes);
2510
+
2511
+ return sigHashes.flat();
2512
+ }
2513
+
2514
+ async CalculateEcSignatureHashes(
2515
+ sigHashes: string[],
2516
+ privKey: string,
2517
+ ): Promise<string[]> {
2518
+ const cfdNetwork = await this.GetCfdNetwork();
2519
+
2520
+ const sigsRequestPromises: Promise<string>[] = [];
2521
+
2522
+ for (let i = 0; i < sigHashes.length; i++) {
2523
+ const sigHash = sigHashes[i];
2524
+
2525
+ const calculateEcSignatureRequest: CalculateEcSignatureRequest = {
2526
+ sighash: sigHash,
2527
+ privkeyData: {
2528
+ privkey: privKey,
2529
+ wif: false,
2530
+ network: cfdNetwork,
2531
+ },
2532
+ isGrindR: true,
2533
+ };
2534
+
2535
+ sigsRequestPromises.push(
2536
+ (async () => {
2537
+ const response = await this.getMethod('CalculateEcSignature')(
2538
+ calculateEcSignatureRequest,
2539
+ );
2540
+ return response.signature;
2541
+ })(),
2542
+ );
2543
+ }
2544
+
2545
+ const sigs: string[] = await Promise.all(sigsRequestPromises);
2546
+
2547
+ return sigs.flat();
2548
+ }
2549
+
2550
+ async VerifySignatures(
2551
+ _dlcOffer: DlcOffer,
2552
+ _dlcAccept: DlcAccept,
2553
+ _dlcTxs: DlcTransactions,
2554
+ _dlcCloses: DlcClose[],
2555
+ rawCloseTxs: string[],
2556
+ isOfferer: boolean,
2557
+ ): Promise<boolean> {
2558
+ const { dlcOffer, dlcAccept, dlcTxs } = checkTypes({
2559
+ _dlcOffer,
2560
+ _dlcAccept,
2561
+ _dlcTxs,
2562
+ });
2563
+
2564
+ const dlcCloses = _dlcCloses.map(
2565
+ (_dlcClose) => checkTypes({ _dlcClose }).dlcClose,
2566
+ );
2567
+
2568
+ const network = await this.getConnectedNetwork();
2569
+
2570
+ const fundingPubKeys =
2571
+ Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
2572
+ ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
2573
+ : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
2574
+
2575
+ const p2ms = payments.p2ms({
2576
+ m: 2,
2577
+ pubkeys: fundingPubKeys,
2578
+ network,
2579
+ });
2580
+
2581
+ const paymentVariant = payments.p2wsh({
2582
+ redeem: p2ms,
2583
+ network,
2584
+ });
2585
+
2586
+ const pubkey = isOfferer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey;
2587
+
2588
+ const sigsValidity: Promise<boolean>[] = [];
2589
+
2590
+ for (let i = 0; i < rawCloseTxs.length; i++) {
2591
+ const rawTx = rawCloseTxs[i];
2592
+ const dlcClose = dlcCloses[i];
2593
+
2594
+ const verifySignatureRequest: VerifySignatureRequest = {
2595
+ tx: rawTx,
2596
+ txin: {
2597
+ txid: dlcTxs.fundTx.txId.serialize().reverse().toString('hex'),
2598
+ vout: dlcTxs.fundTxVout,
2599
+ signature: dlcClose.closeSignature.toString('hex'),
2600
+ pubkey: pubkey.toString('hex'),
2601
+ redeemScript: paymentVariant.redeem.output.toString('hex'),
2602
+ hashType: 'p2wsh',
2603
+ sighashType: 'all',
2604
+ sighashAnyoneCanPay: false,
2605
+ amount: Number(this.getFundOutputValueSats(dlcTxs)),
2606
+ },
2607
+ };
2608
+
2609
+ sigsValidity.push(
2610
+ (async () => {
2611
+ const response = await this.getMethod('VerifySignature')(
2612
+ verifySignatureRequest,
2613
+ );
2614
+ return response.success;
2615
+ })(),
2616
+ );
2617
+ }
2618
+
2619
+ const areSigsValid = (await Promise.all(sigsValidity)).every((b) => b);
2620
+ return areSigsValid;
2621
+ }
2622
+
2623
+ /**
2624
+ * Check whether wallet is offerer of DlcOffer or DlcAccept
2625
+ * @param dlcOffer Dlc Offer Message
2626
+ * @param dlcAccept Dlc Accept Message
2627
+ * @returns {Promise<boolean>}
2628
+ */
2629
+ async isOfferer(
2630
+ _dlcOffer: DlcOffer,
2631
+ _dlcAccept: DlcAccept,
2632
+ ): Promise<boolean> {
2633
+ const { dlcOffer, dlcAccept } = checkTypes({
2634
+ _dlcOffer,
2635
+ _dlcAccept,
2636
+ });
2637
+ const network = await this.getConnectedNetwork();
2638
+
2639
+ const offerFundingSPK = Script.p2wpkhLock(hash160(dlcOffer.fundingPubkey))
2640
+ .serialize()
2641
+ .slice(1);
2642
+ const acceptFundingSPK = Script.p2wpkhLock(hash160(dlcAccept.fundingPubkey))
2643
+ .serialize()
2644
+ .slice(1);
2645
+
2646
+ const offerFundingAddress: string = address.fromOutputScript(
2647
+ offerFundingSPK,
2648
+ network,
2649
+ );
2650
+
2651
+ const acceptFundingAddress: string = address.fromOutputScript(
2652
+ acceptFundingSPK,
2653
+ network,
2654
+ );
2655
+
2656
+ let walletAddress: Address = await this.client.wallet.findAddress([
2657
+ offerFundingAddress,
2658
+ ]);
2659
+ if (walletAddress) return true;
2660
+ walletAddress = await this.client.wallet.findAddress([
2661
+ acceptFundingAddress,
2662
+ ]);
2663
+ if (walletAddress) return false;
2664
+
2665
+ throw Error('Wallet Address not found for DlcOffer or DlcAccept');
2666
+ }
2667
+
2668
+ /**
2669
+ * Create DLC Offer Message
2670
+ * @param contractInfo ContractInfo TLV (V0 or V1)
2671
+ * @param offerCollateralSatoshis Amount DLC Initiator is putting into the contract
2672
+ * @param feeRatePerVb Fee rate in satoshi per virtual byte that both sides use to compute fees in funding tx
2673
+ * @param cetLocktime The nLockTime to be put on CETs
2674
+ * @param refundLocktime The nLockTime to be put on the refund transaction
2675
+ * @param fixedInputs Optional fixed inputs - can be Input[] for regular inputs or FundingInput[] for DLC inputs
2676
+ * @returns {Promise<DlcOffer>}
2677
+ */
2678
+ async createDlcOffer(
2679
+ contractInfo: ContractInfo,
2680
+ offerCollateralSatoshis: bigint,
2681
+ feeRatePerVb: bigint,
2682
+ cetLocktime: number,
2683
+ refundLocktime: number,
2684
+ fixedInputs?: Input[] | FundingInput[],
2685
+ inputSupplementationMode?: InputSupplementationMode,
2686
+ ): Promise<DlcOffer> {
2687
+ contractInfo.validate();
2688
+ const network = await this.getConnectedNetwork();
2689
+
2690
+ const dlcOffer = new DlcOffer();
2691
+
2692
+ // Generate a random 32-byte temporary contract ID
2693
+ dlcOffer.temporaryContractId = crypto.randomBytes(32);
2694
+
2695
+ // Check if we have FundingInput[] (DLC inputs) or Input[] (regular inputs)
2696
+ const hasFundingInputs =
2697
+ fixedInputs && fixedInputs.length > 0 && 'prevTx' in fixedInputs[0]; // FundingInput has prevTx, Input doesn't
2698
+
2699
+ let fundingPubKey: Buffer;
2700
+ let payoutSPK: Buffer;
2701
+ let payoutSerialId: bigint;
2702
+ let _fundingInputs: FundingInput[];
2703
+ let changeSPK: Buffer;
2704
+ let changeSerialId: bigint;
2705
+
2706
+ if (hasFundingInputs) {
2707
+ // Handle FundingInput[] directly (for DLC inputs)
2708
+ const fundingInputs = fixedInputs as FundingInput[];
2709
+
2710
+ // Generate addresses directly since we're bypassing Initialize()
2711
+ const payoutAddress: Address =
2712
+ await this.client.wallet.getUnusedAddress(false);
2713
+ payoutSPK = address.toOutputScript(payoutAddress.address, network);
2714
+
2715
+ const changeAddress: Address =
2716
+ await this.client.wallet.getUnusedAddress(true);
2717
+ changeSPK = address.toOutputScript(changeAddress.address, network);
2718
+
2719
+ const fundingAddress: Address =
2720
+ await this.client.wallet.getUnusedAddress(false);
2721
+ fundingPubKey = Buffer.from(fundingAddress.publicKey, 'hex');
2722
+
2723
+ if (fundingAddress.address === payoutAddress.address)
2724
+ throw Error('Address reuse');
2725
+
2726
+ payoutSerialId = generateSerialId();
2727
+ changeSerialId = generateSerialId();
2728
+ _fundingInputs = fundingInputs;
2729
+ } else {
2730
+ // Handle Input[] through existing Initialize() flow
2731
+ const initResult = await this.Initialize(
2732
+ offerCollateralSatoshis,
2733
+ feeRatePerVb,
2734
+ fixedInputs as Input[],
2735
+ inputSupplementationMode || InputSupplementationMode.Required,
2736
+ );
2737
+
2738
+ fundingPubKey = initResult.fundingPubKey;
2739
+ payoutSPK = initResult.payoutSPK;
2740
+ payoutSerialId = initResult.payoutSerialId;
2741
+ _fundingInputs = initResult.fundingInputs;
2742
+ changeSPK = initResult.changeSPK;
2743
+ changeSerialId = initResult.changeSerialId;
2744
+ }
2745
+
2746
+ _fundingInputs.forEach((input) =>
2747
+ assert(
2748
+ input.type === MessageType.FundingInput,
2749
+ 'FundingInput must be V0',
2750
+ ),
2751
+ );
2752
+
2753
+ const fundingInputs: FundingInput[] = _fundingInputs.map(
2754
+ (input) => input as FundingInput,
2755
+ );
2756
+
2757
+ fundingInputs.sort(
2758
+ (a, b) => Number(a.inputSerialId) - Number(b.inputSerialId),
2759
+ );
2760
+
2761
+ const fundOutputSerialId = generateSerialId();
2762
+
2763
+ assert(
2764
+ changeSerialId !== fundOutputSerialId,
2765
+ 'changeSerialId cannot equal the fundOutputSerialId',
2766
+ );
2767
+
2768
+ dlcOffer.contractFlags = Buffer.from('00', 'hex');
2769
+ dlcOffer.chainHash = chainHashFromNetwork(network);
2770
+ dlcOffer.contractInfo = contractInfo;
2771
+ dlcOffer.fundingPubkey = fundingPubKey;
2772
+ dlcOffer.payoutSpk = payoutSPK;
2773
+ dlcOffer.payoutSerialId = payoutSerialId;
2774
+ dlcOffer.offerCollateral = offerCollateralSatoshis;
2775
+ dlcOffer.fundingInputs = fundingInputs;
2776
+ dlcOffer.changeSpk = changeSPK;
2777
+ dlcOffer.changeSerialId = changeSerialId;
2778
+ dlcOffer.fundOutputSerialId = dlcOffer.fundOutputSerialId =
2779
+ fundOutputSerialId;
2780
+ dlcOffer.feeRatePerVb = feeRatePerVb;
2781
+ dlcOffer.cetLocktime = cetLocktime;
2782
+ dlcOffer.refundLocktime = refundLocktime;
2783
+
2784
+ if (offerCollateralSatoshis === dlcOffer.contractInfo.totalCollateral) {
2785
+ dlcOffer.markAsSingleFunded();
2786
+ }
2787
+
2788
+ assert(
2789
+ (() => {
2790
+ const finalizer = new DualFundingTxFinalizer(
2791
+ dlcOffer.fundingInputs,
2792
+ dlcOffer.payoutSpk,
2793
+ dlcOffer.changeSpk,
2794
+ null,
2795
+ null,
2796
+ null,
2797
+ dlcOffer.feeRatePerVb,
2798
+ );
2799
+ const funding = fundingInputs.reduce((total, input) => {
2800
+ return total + input.prevTx.outputs[input.prevTxVout].value.sats;
2801
+ }, BigInt(0));
2802
+
2803
+ return funding >= offerCollateralSatoshis + finalizer.offerFees;
2804
+ })(),
2805
+ 'fundingInputs for dlcOffer must be greater than offerCollateralSatoshis plus offerFees',
2806
+ );
2807
+
2808
+ dlcOffer.validate();
2809
+
2810
+ return dlcOffer;
2811
+ }
2812
+
2813
+ /**
2814
+ * Accept DLC Offer (supports single-funded DLCs when accept collateral is 0)
2815
+ * @param _dlcOffer Dlc Offer Message
2816
+ * @param fixedInputs Optional inputs to use for Funding Inputs
2817
+ * @returns {Promise<AcceptDlcOfferResponse}
2818
+ */
2819
+ async acceptDlcOffer(
2820
+ _dlcOffer: DlcOffer,
2821
+ fixedInputs?: Input[] | FundingInput[],
2822
+ ): Promise<AcceptDlcOfferResponse> {
2823
+ const { dlcOffer } = checkTypes({ _dlcOffer });
2824
+ dlcOffer.validate();
2825
+
2826
+ const acceptCollateralSatoshis =
2827
+ dlcOffer.contractInfo.totalCollateral - dlcOffer.offerCollateral;
2828
+
2829
+ assert(
2830
+ acceptCollateralSatoshis ===
2831
+ dlcOffer.contractInfo.totalCollateral - dlcOffer.offerCollateral,
2832
+ 'acceptCollaterialSatoshis should equal totalCollateral - offerCollateralSatoshis',
2833
+ );
2834
+
2835
+ let fundingPubKey: Buffer;
2836
+ let payoutSPK: Buffer;
2837
+ let payoutSerialId: bigint;
2838
+ let fundingInputs: FundingInput[];
2839
+ let changeSPK: Buffer;
2840
+ let changeSerialId: bigint;
2841
+
2842
+ if (acceptCollateralSatoshis === BigInt(0)) {
2843
+ // Single-funded DLC: accept side provides no funding
2844
+ const network = await this.getConnectedNetwork();
2845
+
2846
+ // Still need payout address for receiving DLC outcomes
2847
+ const payoutAddress: Address =
2848
+ await this.client.wallet.getUnusedAddress(false);
2849
+ payoutSPK = address.toOutputScript(payoutAddress.address, network);
2850
+
2851
+ // Generate funding pubkey for DLC contract construction
2852
+ const fundingAddress: Address =
2853
+ await this.client.wallet.getUnusedAddress(false);
2854
+ fundingPubKey = Buffer.from(fundingAddress.publicKey, 'hex');
2855
+
2856
+ // Generate change address (even though not used)
2857
+ const changeAddress: Address =
2858
+ await this.client.wallet.getUnusedAddress(true);
2859
+ changeSPK = address.toOutputScript(changeAddress.address, network);
2860
+
2861
+ if (fundingAddress.address === payoutAddress.address)
2862
+ throw Error('Address reuse');
2863
+
2864
+ // Generate serial IDs
2865
+ payoutSerialId = generateSerialId();
2866
+ changeSerialId = generateSerialId();
2867
+
2868
+ // No funding inputs for single-funded DLC
2869
+ fundingInputs = [];
2870
+ } else {
2871
+ // Standard DLC: accept side provides funding
2872
+
2873
+ // Check if we have FundingInput[] (DLC inputs) or Input[] (regular inputs)
2874
+ const hasFundingInputs =
2875
+ fixedInputs && fixedInputs.length > 0 && 'prevTx' in fixedInputs[0]; // FundingInput has prevTx, Input doesn't
2876
+
2877
+ let initResult: InitializeResponse;
2878
+
2879
+ if (hasFundingInputs) {
2880
+ // Handle FundingInput[] directly (for DLC inputs)
2881
+ const fundingInputs = fixedInputs as FundingInput[];
2882
+ const network = await this.getConnectedNetwork();
2883
+
2884
+ // Generate addresses directly since we're bypassing Initialize()
2885
+ const payoutAddress: Address =
2886
+ await this.client.wallet.getUnusedAddress(false);
2887
+ const payoutSPK = address.toOutputScript(
2888
+ payoutAddress.address,
2889
+ network,
2890
+ );
2891
+
2892
+ const changeAddress: Address =
2893
+ await this.client.wallet.getUnusedAddress(true);
2894
+ const changeSPK = address.toOutputScript(
2895
+ changeAddress.address,
2896
+ network,
2897
+ );
2898
+
2899
+ const fundingAddress: Address =
2900
+ await this.client.wallet.getUnusedAddress(false);
2901
+ const fundingPubKey = Buffer.from(fundingAddress.publicKey, 'hex');
2902
+
2903
+ if (fundingAddress.address === payoutAddress.address)
2904
+ throw Error('Address reuse');
2905
+
2906
+ const payoutSerialId = generateSerialId();
2907
+ const changeSerialId = generateSerialId();
2908
+
2909
+ initResult = {
2910
+ fundingPubKey,
2911
+ payoutSPK,
2912
+ payoutSerialId,
2913
+ fundingInputs,
2914
+ changeSPK,
2915
+ changeSerialId,
2916
+ };
2917
+ } else {
2918
+ // Handle Input[] through existing Initialize() flow
2919
+ // Use InputSupplementationMode.None when fixed inputs are provided
2920
+ // to avoid wallet lookup issues with unusual addresses
2921
+ const supplementationMode =
2922
+ fixedInputs && fixedInputs.length > 0
2923
+ ? InputSupplementationMode.None
2924
+ : InputSupplementationMode.Required;
2925
+
2926
+ initResult = await this.Initialize(
2927
+ acceptCollateralSatoshis,
2928
+ dlcOffer.feeRatePerVb,
2929
+ fixedInputs as Input[],
2930
+ supplementationMode,
2931
+ );
2932
+ }
2933
+
2934
+ fundingPubKey = initResult.fundingPubKey;
2935
+ payoutSPK = initResult.payoutSPK;
2936
+ payoutSerialId = initResult.payoutSerialId;
2937
+ changeSPK = initResult.changeSPK;
2938
+ changeSerialId = initResult.changeSerialId;
2939
+
2940
+ const _fundingInputs = initResult.fundingInputs;
2941
+
2942
+ _fundingInputs.forEach((input) =>
2943
+ assert(
2944
+ input.type === MessageType.FundingInput,
2945
+ 'FundingInput must be V0',
2946
+ ),
2947
+ );
2948
+
2949
+ fundingInputs = _fundingInputs.map((input) => input as FundingInput);
2950
+
2951
+ fundingInputs.sort(
2952
+ (a, b) => Number(a.inputSerialId) - Number(b.inputSerialId),
2953
+ );
2954
+ }
2955
+
2956
+ assert(
2957
+ Buffer.compare(dlcOffer.fundingPubkey, fundingPubKey) !== 0,
2958
+ 'DlcOffer and DlcAccept FundingPubKey cannot be the same',
2959
+ );
2960
+
2961
+ const dlcAccept = new DlcAccept();
2962
+
2963
+ dlcAccept.temporaryContractId = sha256(dlcOffer.serialize());
2964
+ dlcAccept.acceptCollateral = acceptCollateralSatoshis;
2965
+ dlcAccept.fundingPubkey = fundingPubKey;
2966
+ dlcAccept.payoutSpk = payoutSPK;
2967
+ dlcAccept.payoutSerialId = payoutSerialId;
2968
+ dlcAccept.fundingInputs = fundingInputs;
2969
+ dlcAccept.changeSpk = changeSPK;
2970
+ dlcAccept.changeSerialId = changeSerialId;
2971
+
2972
+ assert(
2973
+ dlcAccept.changeSerialId !== dlcOffer.fundOutputSerialId,
2974
+ 'changeSerialId cannot equal the fundOutputSerialId',
2975
+ );
2976
+
2977
+ assert(
2978
+ dlcOffer.payoutSerialId !== dlcAccept.payoutSerialId,
2979
+ 'offer.payoutSerialId cannot equal accept.payoutSerialId',
2980
+ );
2981
+
2982
+ assert(
2983
+ (() => {
2984
+ const ids = [
2985
+ dlcOffer.changeSerialId,
2986
+ dlcAccept.changeSerialId,
2987
+ dlcOffer.fundOutputSerialId,
2988
+ ];
2989
+ return new Set(ids).size === ids.length;
2990
+ })(),
2991
+ 'offer.changeSerialID, accept.changeSerialId and fundOutputSerialId must be unique',
2992
+ );
2993
+
2994
+ if (dlcOffer.singleFunded) dlcAccept.markAsSingleFunded();
2995
+
2996
+ dlcAccept.validate();
2997
+
2998
+ // Only validate funding requirements if accept side is providing collateral
2999
+ if (acceptCollateralSatoshis > BigInt(0)) {
3000
+ assert(
3001
+ (() => {
3002
+ const finalizer = new DualFundingTxFinalizer(
3003
+ dlcOffer.fundingInputs,
3004
+ dlcOffer.payoutSpk,
3005
+ dlcOffer.changeSpk,
3006
+ dlcAccept.fundingInputs,
3007
+ dlcAccept.payoutSpk,
3008
+ dlcAccept.changeSpk,
3009
+ dlcOffer.feeRatePerVb,
3010
+ );
3011
+ const funding = fundingInputs.reduce((total, input) => {
3012
+ return total + input.prevTx.outputs[input.prevTxVout].value.sats;
3013
+ }, BigInt(0));
3014
+
3015
+ return funding >= acceptCollateralSatoshis + finalizer.acceptFees;
3016
+ })(),
3017
+ 'fundingInputs for dlcAccept must be greater than acceptCollateralSatoshis plus acceptFees',
3018
+ );
3019
+ }
3020
+
3021
+ const { dlcTransactions, messagesList } = await this.createDlcTxs(
3022
+ dlcOffer,
3023
+ dlcAccept,
3024
+ );
3025
+
3026
+ const { cetSignatures, refundSignature } =
3027
+ await this.CreateCetAdaptorAndRefundSigs(
3028
+ dlcOffer,
3029
+ dlcAccept,
3030
+ dlcTransactions,
3031
+ messagesList,
3032
+ false,
3033
+ );
3034
+
3035
+ const _dlcTransactions = dlcTransactions;
3036
+
3037
+ const contractId = xor(
3038
+ _dlcTransactions.fundTx.txId.serialize(),
3039
+ dlcAccept.temporaryContractId,
3040
+ );
3041
+ _dlcTransactions.contractId = contractId;
3042
+
3043
+ dlcAccept.cetAdaptorSignatures = cetSignatures;
3044
+ dlcAccept.refundSignature = refundSignature;
3045
+
3046
+ return { dlcAccept, dlcTransactions: _dlcTransactions };
3047
+ }
3048
+
3049
+ /**
3050
+ * Sign Dlc Accept Message
3051
+ * @param _dlcOffer Dlc Offer Message
3052
+ * @param _dlcAccept Dlc Accept Message
3053
+ * @returns {Promise<SignDlcAcceptResponse}
3054
+ */
3055
+ async signDlcAccept(
3056
+ dlcOffer: DlcOffer,
3057
+ dlcAccept: DlcAccept,
3058
+ ): Promise<SignDlcAcceptResponse> {
3059
+ dlcOffer.validate();
3060
+ dlcAccept.validate();
3061
+
3062
+ assert(
3063
+ Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) !== 0,
3064
+ 'DlcOffer and DlcAccept FundingPubKey cannot be the same',
3065
+ );
3066
+
3067
+ const dlcSign = new DlcSign();
3068
+
3069
+ const { dlcTransactions, messagesList } = await this.createDlcTxs(
3070
+ dlcOffer,
3071
+ dlcAccept,
3072
+ );
3073
+
3074
+ await this.VerifyCetAdaptorAndRefundSigs(
3075
+ dlcOffer,
3076
+ dlcAccept,
3077
+ dlcSign,
3078
+ dlcTransactions,
3079
+ messagesList,
3080
+ true,
3081
+ );
3082
+
3083
+ const { cetSignatures, refundSignature } =
3084
+ await this.CreateCetAdaptorAndRefundSigs(
3085
+ dlcOffer,
3086
+ dlcAccept,
3087
+ dlcTransactions,
3088
+ messagesList,
3089
+ true,
3090
+ );
3091
+
3092
+ const fundingSignatures = await this.CreateFundingSigsAlt(
3093
+ dlcOffer,
3094
+ dlcAccept,
3095
+ dlcTransactions,
3096
+ true,
3097
+ );
3098
+
3099
+ const dlcTxs = dlcTransactions;
3100
+
3101
+ const contractId = xor(
3102
+ dlcTxs.fundTx.txId.serialize(),
3103
+ dlcAccept.temporaryContractId,
3104
+ );
3105
+
3106
+ assert(
3107
+ Buffer.compare(
3108
+ contractId,
3109
+ xor(dlcTxs.fundTx.txId.serialize(), dlcAccept.temporaryContractId),
3110
+ ) === 0,
3111
+ 'contractId must be the xor of funding txid, fundingOutputIndex and the tempContractId',
3112
+ );
3113
+
3114
+ dlcTxs.contractId = contractId;
3115
+
3116
+ dlcSign.contractId = contractId;
3117
+ dlcSign.cetAdaptorSignatures = cetSignatures;
3118
+ dlcSign.refundSignature = refundSignature;
3119
+ dlcSign.fundingSignatures = fundingSignatures;
3120
+
3121
+ return { dlcSign, dlcTransactions: dlcTxs };
3122
+ }
3123
+
3124
+ /**
3125
+ * Finalize Dlc Sign
3126
+ * @param dlcOffer Dlc Offer Message
3127
+ * @param dlcAccept Dlc Accept Message
3128
+ * @param dlcSign Dlc Sign Message
3129
+ * @param dlcTxs Dlc Transactions Message
3130
+ * @returns {Promise<Tx>}
3131
+ */
3132
+ async finalizeDlcSign(
3133
+ dlcOffer: DlcOffer,
3134
+ dlcAccept: DlcAccept,
3135
+ dlcSign: DlcSign,
3136
+ dlcTxs: DlcTransactions,
3137
+ ): Promise<Tx> {
3138
+ let messagesList: Messages[] = [];
3139
+
3140
+ if (
3141
+ dlcOffer.contractInfo.type === MessageType.SingleContractInfo &&
3142
+ (dlcOffer.contractInfo as SingleContractInfo).contractDescriptor.type ===
3143
+ MessageType.SingleContractInfo
3144
+ ) {
3145
+ for (const outcome of (
3146
+ (dlcOffer.contractInfo as SingleContractInfo)
3147
+ .contractDescriptor as EnumeratedDescriptor
3148
+ ).outcomes) {
3149
+ messagesList.push({ messages: [outcome.outcome] });
3150
+ }
3151
+ } else {
3152
+ const payoutResponses = this.GetPayouts(dlcOffer);
3153
+ const { messagesList: oracleEventMessagesList } =
3154
+ this.FlattenPayouts(payoutResponses);
3155
+ messagesList = oracleEventMessagesList;
3156
+ }
3157
+
3158
+ await this.VerifyCetAdaptorAndRefundSigs(
3159
+ dlcOffer,
3160
+ dlcAccept,
3161
+ dlcSign,
3162
+ dlcTxs,
3163
+ messagesList,
3164
+ false,
3165
+ );
3166
+
3167
+ await this.VerifyFundingSigsAlt(
3168
+ dlcOffer,
3169
+ dlcAccept,
3170
+ dlcSign,
3171
+ dlcTxs,
3172
+ false,
3173
+ );
3174
+
3175
+ const fundingSignatures = await this.CreateFundingSigsAlt(
3176
+ dlcOffer,
3177
+ dlcAccept,
3178
+ dlcTxs,
3179
+ false,
3180
+ );
3181
+
3182
+ const fundTx = await this.CreateFundingTx(
3183
+ dlcOffer,
3184
+ dlcAccept,
3185
+ dlcSign,
3186
+ dlcTxs,
3187
+ fundingSignatures,
3188
+ );
3189
+
3190
+ return fundTx;
3191
+ }
3192
+
3193
+ /**
3194
+ * Execute DLC
3195
+ * @param _dlcOffer Dlc Offer Message
3196
+ * @param _dlcAccept Dlc Accept Message
3197
+ * @param _dlcSign Dlc Sign Message
3198
+ * @param _dlcTxs Dlc Transactions Message
3199
+ * @param oracleAttestation Oracle Attestations TLV (V0)
3200
+ * @param isOfferer Whether party is Dlc Offerer
3201
+ * @returns {Promise<Tx>}
3202
+ */
3203
+ async execute(
3204
+ dlcOffer: DlcOffer,
3205
+ dlcAccept: DlcAccept,
3206
+ dlcSign: DlcSign,
3207
+ dlcTxs: DlcTransactions,
3208
+ oracleAttestation: OracleAttestation,
3209
+ isOfferer?: boolean,
3210
+ ): Promise<Tx> {
3211
+ if (isOfferer === undefined)
3212
+ isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
3213
+
3214
+ this.ValidateEvent(dlcOffer, oracleAttestation);
3215
+
3216
+ return this.FindAndSignCet(
3217
+ dlcOffer,
3218
+ dlcAccept,
3219
+ dlcSign,
3220
+ dlcTxs,
3221
+ oracleAttestation,
3222
+ isOfferer,
3223
+ );
3224
+ }
3225
+
3226
+ /**
3227
+ * Refund DLC
3228
+ * @param dlcOffer Dlc Offer Message
3229
+ * @param dlcAccept Dlc Accept Message
3230
+ * @param dlcSign Dlc Sign Message
3231
+ * @param dlcTxs Dlc Transactions message
3232
+ * @returns {Promise<Tx>}
3233
+ */
3234
+ async refund(
3235
+ dlcOffer: DlcOffer,
3236
+ dlcAccept: DlcAccept,
3237
+ dlcSign: DlcSign,
3238
+ dlcTxs: DlcTransactions,
3239
+ ): Promise<Tx> {
3240
+ const network = await this.getConnectedNetwork();
3241
+ const psbt = new Psbt({ network });
3242
+
3243
+ // Verify refund transaction locktime matches expected
3244
+ if (Number(dlcTxs.refundTx.locktime) !== dlcOffer.refundLocktime) {
3245
+ throw new Error(
3246
+ `Refund transaction locktime ${dlcTxs.refundTx.locktime} does not match expected ${dlcOffer.refundLocktime}`,
3247
+ );
3248
+ }
3249
+
3250
+ // Add the funding input
3251
+ const fundingOutput = dlcTxs.fundTx.outputs[dlcTxs.fundTxVout];
3252
+ psbt.addInput({
3253
+ hash: dlcTxs.fundTx.txId.toString(),
3254
+ index: dlcTxs.fundTxVout,
3255
+ sequence: 0,
3256
+ witnessUtxo: {
3257
+ script: fundingOutput.scriptPubKey.serialize().subarray(1), // Remove length prefix
3258
+ value: Number(fundingOutput.value.sats),
3259
+ },
3260
+ witnessScript: this.CreateFundingScript(dlcOffer, dlcAccept), // 2-of-2 multisig script
3261
+ });
3262
+
3263
+ // Add the refund output
3264
+ const refundOutput = dlcTxs.refundTx.outputs[0];
3265
+ psbt.addOutput({
3266
+ address: address.fromOutputScript(
3267
+ refundOutput.scriptPubKey.serialize().subarray(1),
3268
+ network,
3269
+ ),
3270
+ value: Number(refundOutput.value.sats),
3271
+ });
3272
+
3273
+ // Set the locktime to match the refund transaction
3274
+ psbt.setLocktime(Number(dlcTxs.refundTx.locktime));
3275
+
3276
+ // Sort funding pubkeys for consistent signature ordering
3277
+ const fundingPubKeys =
3278
+ Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
3279
+ ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
3280
+ : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
3281
+
3282
+ // Add both refund signatures as partial signatures
3283
+ psbt.updateInput(0, {
3284
+ partialSig: [
3285
+ { pubkey: fundingPubKeys[0], signature: dlcSign.refundSignature },
3286
+ { pubkey: fundingPubKeys[1], signature: dlcAccept.refundSignature },
3287
+ ],
3288
+ });
3289
+
3290
+ // Validate all signatures
3291
+ psbt.validateSignaturesOfInput(
3292
+ 0,
3293
+ (pubkey: Buffer, msghash: Buffer, signature: Buffer) => {
3294
+ return ecc.verify(msghash, pubkey, signature);
3295
+ },
3296
+ );
3297
+
3298
+ // Finalize the input - this will create the final witness script
3299
+ psbt.finalizeInput(0);
3300
+
3301
+ // Extract the final transaction
3302
+ const finalTx = psbt.extractTransaction();
3303
+
3304
+ // Convert to the expected Tx format
3305
+ return Tx.decode(StreamReader.fromBuffer(finalTx.toBuffer()));
3306
+ }
3307
+
3308
+ /**
3309
+ * Goal of createDlcClose is for alice (the initiator) to
3310
+ * 1. take dlcoffer, accept, and sign messages. Create a dlcClose message.
3311
+ * 2. Build a close tx, sign.
3312
+ * 3. return dlcClose message (no psbt)
3313
+ */
3314
+
3315
+ /**
3316
+ * Generate DlcClose messagetype for closing DLC with Mutual Consent
3317
+ * @param _dlcOffer DlcOffer TLV (V0)
3318
+ * @param _dlcAccept DlcAccept TLV (V0)
3319
+ * @param _dlcTxs DlcTransactions TLV (V0)
3320
+ * @param initiatorPayoutSatoshis Amount initiator expects as a payout
3321
+ * @param isOfferer Whether offerer or not
3322
+ * @param _inputs Optionally specified closing inputs
3323
+ * @returns {Promise<DlcClose>}
3324
+ */
3325
+ async createDlcClose(
3326
+ _dlcOffer: DlcOffer,
3327
+ _dlcAccept: DlcAccept,
3328
+ _dlcTxs: DlcTransactions,
3329
+ initiatorPayoutSatoshis: bigint,
3330
+ isOfferer?: boolean,
3331
+ _inputs?: Input[],
3332
+ ): Promise<DlcClose> {
3333
+ const { dlcOffer, dlcAccept, dlcTxs } = checkTypes({
3334
+ _dlcOffer,
3335
+ _dlcAccept,
3336
+ _dlcTxs,
3337
+ });
3338
+
3339
+ if (isOfferer === undefined)
3340
+ isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
3341
+
3342
+ const network = await this.getConnectedNetwork();
3343
+ const psbt = new Psbt({ network });
3344
+
3345
+ const fundingPubKeys =
3346
+ Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
3347
+ ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
3348
+ : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
3349
+
3350
+ const p2ms = payments.p2ms({
3351
+ m: 2,
3352
+ pubkeys: fundingPubKeys,
3353
+ network,
3354
+ });
3355
+
3356
+ const paymentVariant = payments.p2wsh({
3357
+ redeem: p2ms,
3358
+ network,
3359
+ });
3360
+
3361
+ // Initiate and build PSBT
3362
+ let inputs: Input[] = _inputs;
3363
+ if (!_inputs) {
3364
+ const tempInputs = await this.GetInputsForAmountWithMode(
3365
+ [BigInt(20000)],
3366
+ dlcOffer.feeRatePerVb,
3367
+ _inputs || [],
3368
+ InputSupplementationMode.Required,
3369
+ );
3370
+ _inputs = tempInputs;
3371
+ }
3372
+ // Ensure all inputs have derivation paths by fetching from wallet
3373
+ const inputsWithPaths: { input: Input; address: Address }[] =
3374
+ await Promise.all(
3375
+ _inputs.map(async (input) => {
3376
+ const address: Address = await this.getMethod('getWalletAddress')(
3377
+ input.address,
3378
+ );
3379
+ const inputWithPath = new Input(
3380
+ input.txid,
3381
+ input.vout,
3382
+ input.address,
3383
+ input.amount,
3384
+ input.value,
3385
+ input.derivationPath || address.derivationPath, // Use derivationPath from wallet if not set
3386
+ input.maxWitnessLength,
3387
+ input.redeemScript,
3388
+ input.inputSerialId || generateSerialId(),
3389
+ input.scriptPubKey,
3390
+ input.label,
3391
+ input.confirmations,
3392
+ input.spendable,
3393
+ input.solvable,
3394
+ input.safe,
3395
+ input.dlcInput,
3396
+ );
3397
+ return { input: inputWithPath, address };
3398
+ }),
3399
+ );
3400
+
3401
+ inputs = inputsWithPaths.map((item) => item.input);
3402
+ const pubkeys: Buffer[] = inputsWithPaths.map((item) =>
3403
+ Buffer.from(item.address.publicKey, 'hex'),
3404
+ );
3405
+
3406
+ const fundingInputSerialId = generateSerialId();
3407
+
3408
+ // Make temporary array to hold all inputs and then sort them
3409
+ // this method can be improved later
3410
+ const psbtInputs = [];
3411
+ psbtInputs.push({
3412
+ hash: dlcTxs.fundTx.txId.serialize(),
3413
+ index: dlcTxs.fundTxVout,
3414
+ sequence: 0,
3415
+ witnessUtxo: {
3416
+ script: paymentVariant.output,
3417
+ value: Number(this.getFundOutputValueSats(dlcTxs)),
3418
+ },
3419
+ witnessScript: paymentVariant.redeem.output,
3420
+ inputSerialId: fundingInputSerialId,
3421
+ derivationPath: null,
3422
+ });
3423
+
3424
+ // add all dlc close inputs
3425
+ inputs.forEach((input, i) => {
3426
+ const paymentVariant = payments.p2wpkh({ pubkey: pubkeys[i], network });
3427
+
3428
+ psbtInputs.push({
3429
+ hash: input.txid,
3430
+ index: input.vout,
3431
+ sequence: 0,
3432
+ witnessUtxo: {
3433
+ script: paymentVariant.output,
3434
+ value: input.value,
3435
+ },
3436
+ inputSerialId: input.inputSerialId,
3437
+ derivationPath: input.derivationPath,
3438
+ });
3439
+ });
3440
+
3441
+ // sort all inputs in ascending order by serial ID
3442
+ // The only reason we are doing this is for privacy. If the fundingInput is
3443
+ // always first, it is very obvious. Hence, a serialId is randomly generated
3444
+ // and the inputs are sorted by that instead.
3445
+ const sortedPsbtInputs = psbtInputs.sort((a, b) =>
3446
+ Number(a.inputSerialId - b.inputSerialId),
3447
+ );
3448
+
3449
+ // Get index of fundingInput
3450
+ const fundingInputIndex = sortedPsbtInputs.findIndex(
3451
+ (input) => input.inputSerialId === fundingInputSerialId,
3452
+ );
3453
+
3454
+ // add to psbt
3455
+ sortedPsbtInputs.forEach((input) => psbt.addInput(input));
3456
+
3457
+ const fundingInputs: FundingInput[] = await Promise.all(
3458
+ inputs.map(async (input) => {
3459
+ return this.inputToFundingInput(input);
3460
+ }),
3461
+ );
3462
+
3463
+ const finalizer = new DualClosingTxFinalizer(
3464
+ fundingInputs,
3465
+ dlcOffer.payoutSpk,
3466
+ dlcAccept.payoutSpk,
3467
+ dlcOffer.feeRatePerVb,
3468
+ );
3469
+
3470
+ const closeInputAmount = BigInt(
3471
+ inputs.reduce((acc, val) => acc + val.value, 0),
3472
+ );
3473
+
3474
+ const offerPayoutValue: bigint = isOfferer
3475
+ ? closeInputAmount +
3476
+ initiatorPayoutSatoshis -
3477
+ finalizer.offerInitiatorFees
3478
+ : dlcOffer.contractInfo.totalCollateral - initiatorPayoutSatoshis;
3479
+
3480
+ const acceptPayoutValue: bigint = isOfferer
3481
+ ? dlcOffer.contractInfo.totalCollateral - initiatorPayoutSatoshis
3482
+ : closeInputAmount +
3483
+ initiatorPayoutSatoshis -
3484
+ finalizer.offerInitiatorFees;
3485
+
3486
+ const offerFirst = dlcOffer.payoutSerialId < dlcAccept.payoutSerialId;
3487
+
3488
+ psbt.addOutput({
3489
+ value: Number(offerFirst ? offerPayoutValue : acceptPayoutValue),
3490
+ address: address.fromOutputScript(
3491
+ offerFirst ? dlcOffer.payoutSpk : dlcAccept.payoutSpk,
3492
+ network,
3493
+ ),
3494
+ });
3495
+
3496
+ psbt.addOutput({
3497
+ value: Number(offerFirst ? acceptPayoutValue : offerPayoutValue),
3498
+ address: address.fromOutputScript(
3499
+ offerFirst ? dlcAccept.payoutSpk : dlcOffer.payoutSpk,
3500
+ network,
3501
+ ),
3502
+ });
3503
+
3504
+ // Generate keypair to sign inputs
3505
+ const fundPrivateKeyPair = await this.GetFundKeyPair(
3506
+ dlcOffer,
3507
+ dlcAccept,
3508
+ isOfferer,
3509
+ );
3510
+
3511
+ // Sign dlc fundinginput
3512
+ psbt.signInput(fundingInputIndex, fundPrivateKeyPair);
3513
+
3514
+ // Sign dlcclose inputs
3515
+ await Promise.all(
3516
+ sortedPsbtInputs.map(async (input, i) => {
3517
+ if (i === fundingInputIndex) return;
3518
+
3519
+ // derive keypair
3520
+ if (!input.derivationPath) {
3521
+ throw new Error(`Missing derivation path for input ${i}`);
3522
+ }
3523
+ const keyPair = await this.getMethod('keyPair')(input.derivationPath);
3524
+ psbt.signInput(i, keyPair);
3525
+ }),
3526
+ );
3527
+
3528
+ // Validate signatures
3529
+ psbt.validateSignaturesOfAllInputs(
3530
+ (pubkey: Buffer, msghash: Buffer, signature: Buffer) => {
3531
+ return ecc.verify(msghash, pubkey, signature);
3532
+ },
3533
+ );
3534
+
3535
+ // Extract close signature from psbt and decode it to only extract r and s values
3536
+ const closeSignature = await script.signature.decode(
3537
+ psbt.data.inputs[fundingInputIndex].partialSig[0].signature,
3538
+ ).signature;
3539
+
3540
+ // Extract funding signatures from psbt
3541
+ const inputSigs = psbt.data.inputs
3542
+ .filter((input) => input !== fundingInputIndex)
3543
+ .map((input) => input.partialSig[0]);
3544
+
3545
+ // create fundingSignatures
3546
+ const witnessElements: ScriptWitnessV0[][] = [];
3547
+ for (let i = 0; i < inputSigs.length; i++) {
3548
+ const sigWitness = new ScriptWitnessV0();
3549
+ sigWitness.witness = inputSigs[i].signature;
3550
+ const pubKeyWitness = new ScriptWitnessV0();
3551
+ pubKeyWitness.witness = inputSigs[i].pubkey;
3552
+ witnessElements.push([sigWitness, pubKeyWitness]);
3553
+ }
3554
+ const fundingSignatures = new FundingSignatures();
3555
+ fundingSignatures.witnessElements = witnessElements;
3556
+
3557
+ // Create DlcClose
3558
+ const dlcClose = new DlcClose();
3559
+ dlcClose.contractId = dlcTxs.contractId;
3560
+ dlcClose.offerPayoutSatoshis = BigInt(
3561
+ psbt.txOutputs[offerFirst ? 0 : 1].value,
3562
+ ); // You give collateral back to users
3563
+ dlcClose.acceptPayoutSatoshis = BigInt(
3564
+ psbt.txOutputs[offerFirst ? 1 : 0].value,
3565
+ ); // give collateral back to users
3566
+ dlcClose.fundInputSerialId = fundingInputSerialId; // randomly generated serial id
3567
+ dlcClose.closeSignature = closeSignature;
3568
+ dlcClose.fundingSignatures = fundingSignatures;
3569
+ dlcClose.fundingInputs = fundingInputs as FundingInput[];
3570
+ dlcClose.validate();
3571
+
3572
+ return dlcClose;
3573
+ }
3574
+
3575
+ /**
3576
+ * Generate multiple DlcClose messagetypes for closing DLC with Mutual Consent
3577
+ * @param _dlcOffer DlcOffer TLV (V0)
3578
+ * @param _dlcAccept DlcAccept TLV (V0)
3579
+ * @param _dlcTxs DlcTransactions TLV (V0)
3580
+ * @param initiatorPayouts Array of amounts initiator expects as payouts
3581
+ * @param isOfferer Whether offerer or not
3582
+ * @param _inputs Optionally specified closing inputs
3583
+ * @returns {Promise<DlcClose[]>}
3584
+ */
3585
+ async createBatchDlcClose(
3586
+ _dlcOffer: DlcOffer,
3587
+ _dlcAccept: DlcAccept,
3588
+ _dlcTxs: DlcTransactions,
3589
+ initiatorPayouts: bigint[],
3590
+ isOfferer?: boolean,
3591
+ _inputs?: Input[],
3592
+ ): Promise<DlcClose[]> {
3593
+ const { dlcOffer, dlcAccept, dlcTxs } = checkTypes({
3594
+ _dlcOffer,
3595
+ _dlcAccept,
3596
+ _dlcTxs,
3597
+ });
3598
+
3599
+ if (isOfferer === undefined)
3600
+ isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
3601
+
3602
+ if (_inputs && _inputs.length > 0)
3603
+ throw Error('funding inputs not supported on BatchDlcClose'); // TODO support multiple funding inputs
3604
+
3605
+ const fundingInputSerialId = generateSerialId();
3606
+
3607
+ const fundingInputs: FundingInput[] = []; // TODO: support multiple funding inputs
3608
+
3609
+ const finalizer = new DualClosingTxFinalizer(
3610
+ fundingInputs,
3611
+ dlcOffer.payoutSpk,
3612
+ dlcAccept.payoutSpk,
3613
+ dlcOffer.feeRatePerVb,
3614
+ );
3615
+
3616
+ // Generate keypair to sign inputs
3617
+ const fundPrivateKeyPair = await this.GetFundKeyPair(
3618
+ dlcOffer,
3619
+ dlcAccept,
3620
+ isOfferer,
3621
+ );
3622
+
3623
+ const closeInputAmount = BigInt(0); // TODO support multiple funding inputs
3624
+
3625
+ const privKey = Buffer.from(fundPrivateKeyPair.privateKey).toString('hex');
3626
+
3627
+ const rawCloseTxs = await this.CreateCloseRawTxs(
3628
+ dlcOffer,
3629
+ dlcAccept,
3630
+ dlcTxs,
3631
+ closeInputAmount,
3632
+ isOfferer,
3633
+ [],
3634
+ fundingInputs,
3635
+ initiatorPayouts,
3636
+ );
3637
+
3638
+ const sigHashes = await this.CreateSignatureHashes(
3639
+ dlcOffer,
3640
+ dlcAccept,
3641
+ dlcTxs,
3642
+ rawCloseTxs,
3643
+ );
3644
+
3645
+ const signatures = await this.CalculateEcSignatureHashes(
3646
+ sigHashes,
3647
+ privKey,
3648
+ );
3649
+
3650
+ const dlcCloses = [];
3651
+
3652
+ signatures.forEach((sig, i) => {
3653
+ const payout = initiatorPayouts[i];
3654
+ const payoutMinusOfferFees =
3655
+ finalizer.offerInitiatorFees > payout
3656
+ ? BigInt(0)
3657
+ : payout - finalizer.offerInitiatorFees;
3658
+ const collateralMinusPayout =
3659
+ payout > dlcOffer.contractInfo.totalCollateral
3660
+ ? BigInt(0)
3661
+ : dlcOffer.contractInfo.totalCollateral - payout;
3662
+
3663
+ const offerPayoutValue: bigint = isOfferer
3664
+ ? closeInputAmount + payoutMinusOfferFees
3665
+ : collateralMinusPayout;
3666
+
3667
+ const acceptPayoutValue: bigint = isOfferer
3668
+ ? collateralMinusPayout
3669
+ : closeInputAmount + payoutMinusOfferFees;
3670
+
3671
+ const fundingSignatures = new FundingSignatures();
3672
+
3673
+ const dlcClose = new DlcClose();
3674
+ dlcClose.contractId = dlcTxs.contractId;
3675
+ dlcClose.offerPayoutSatoshis = offerPayoutValue;
3676
+ dlcClose.acceptPayoutSatoshis = acceptPayoutValue;
3677
+ dlcClose.fundInputSerialId = fundingInputSerialId;
3678
+ dlcClose.closeSignature = Buffer.from(sig, 'hex');
3679
+ dlcClose.fundingSignatures = fundingSignatures;
3680
+ dlcClose.validate();
3681
+
3682
+ dlcCloses.push(dlcClose);
3683
+ });
3684
+
3685
+ return dlcCloses;
3686
+ }
3687
+
3688
+ async verifyBatchDlcCloseUsingMetadata(
3689
+ dlcCloseMetadata: DlcCloseMetadata,
3690
+ _dlcCloses: DlcClose[],
3691
+ isOfferer?: boolean,
3692
+ ): Promise<void> {
3693
+ const { dlcOffer, dlcAccept, dlcTxs } = dlcCloseMetadata.toDlcMessages();
3694
+
3695
+ await this.verifyBatchDlcClose(
3696
+ dlcOffer,
3697
+ dlcAccept,
3698
+ dlcTxs,
3699
+ _dlcCloses,
3700
+ isOfferer,
3701
+ );
3702
+ }
3703
+
3704
+ /**
3705
+ * Verify multiple DlcClose messagetypes for closing DLC with Mutual Consent
3706
+ * @param _dlcOffer DlcOffer TLV (V0)
3707
+ * @param _dlcAccept DlcAccept TLV (V0)
3708
+ * @param _dlcTxs DlcTransactions TLV (V0)
3709
+ * @param _dlcCloses DlcClose[] TLV (V0)
3710
+ * @param isOfferer Whether offerer or not
3711
+ * @returns {Promise<void>}
3712
+ */
3713
+ async verifyBatchDlcClose(
3714
+ _dlcOffer: DlcOffer,
3715
+ _dlcAccept: DlcAccept,
3716
+ _dlcTxs: DlcTransactions,
3717
+ _dlcCloses: DlcClose[],
3718
+ isOfferer?: boolean,
3719
+ ): Promise<void> {
3720
+ const { dlcOffer, dlcAccept, dlcTxs } = checkTypes({
3721
+ _dlcOffer,
3722
+ _dlcAccept,
3723
+ _dlcTxs,
3724
+ });
3725
+
3726
+ const dlcCloses = _dlcCloses.map(
3727
+ (_dlcClose) => checkTypes({ _dlcClose }).dlcClose,
3728
+ );
3729
+
3730
+ if (isOfferer === undefined)
3731
+ isOfferer = await this.isOfferer(dlcOffer, dlcAccept);
3732
+
3733
+ assert(
3734
+ dlcCloses.every((dlcClose) => dlcClose.fundingInputs.length === 0),
3735
+ 'funding inputs not supported on verify BatchDlcClose',
3736
+ ); // TODO support multiple funding inputs
3737
+
3738
+ const closeInputAmount = BigInt(0); // TODO support multiple funding inputs
3739
+
3740
+ const rawCloseTxs = await this.CreateCloseRawTxs(
3741
+ dlcOffer,
3742
+ dlcAccept,
3743
+ dlcTxs,
3744
+ closeInputAmount,
3745
+ isOfferer,
3746
+ dlcCloses,
3747
+ );
3748
+
3749
+ const areSigsValid = await this.VerifySignatures(
3750
+ dlcOffer,
3751
+ dlcAccept,
3752
+ dlcTxs,
3753
+ dlcCloses,
3754
+ rawCloseTxs,
3755
+ isOfferer,
3756
+ );
3757
+
3758
+ assert(areSigsValid, 'Signatures invalid in Verify Batch DlcClose');
3759
+ }
3760
+
3761
+ /**
3762
+ * Goal of finalize Dlc Close is for bob to
3763
+ * 1. take the dlcClose created by alice using createDlcClose,
3764
+ * 2. Build a psbt using Alice's dlcClose message
3765
+ * 3. Sign psbt with bob's privkey
3766
+ * 4. return a tx ready to be broadcast
3767
+ */
3768
+
3769
+ /**
3770
+ * Finalize Dlc Close
3771
+ * @param _dlcOffer Dlc Offer Message
3772
+ * @param _dlcAccept Dlc Accept Message
3773
+ * @param _dlcClose Dlc Close Message
3774
+ * @param _dlcTxs Dlc Transactions Message
3775
+ * @returns {Promise<Tx>}
3776
+ */
3777
+ async finalizeDlcClose(
3778
+ _dlcOffer: DlcOffer,
3779
+ _dlcAccept: DlcAccept,
3780
+ _dlcClose: DlcClose,
3781
+ _dlcTxs: DlcTransactions,
3782
+ ): Promise<string> {
3783
+ const { dlcOffer, dlcAccept, dlcClose, dlcTxs } = checkTypes({
3784
+ _dlcOffer,
3785
+ _dlcAccept,
3786
+ _dlcClose,
3787
+ _dlcTxs,
3788
+ });
3789
+
3790
+ dlcOffer.validate();
3791
+ dlcAccept.validate();
3792
+ dlcClose.validate();
3793
+
3794
+ const network = await this.getConnectedNetwork();
3795
+ const psbt = new Psbt({ network });
3796
+
3797
+ const fundingPubKeys =
3798
+ Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
3799
+ ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
3800
+ : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
3801
+
3802
+ const p2ms = payments.p2ms({
3803
+ m: 2,
3804
+ pubkeys: fundingPubKeys,
3805
+ network,
3806
+ });
3807
+
3808
+ const paymentVariant = payments.p2wsh({
3809
+ redeem: p2ms,
3810
+ network,
3811
+ });
3812
+
3813
+ // Make temporary array to hold all inputs and then sort them
3814
+ // this method can be improved later
3815
+ const psbtInputs = [];
3816
+ psbtInputs.push({
3817
+ hash: dlcTxs.fundTx.txId.serialize(),
3818
+ index: dlcTxs.fundTxVout,
3819
+ sequence: 0,
3820
+ witnessUtxo: {
3821
+ script: paymentVariant.output,
3822
+ value: Number(this.getFundOutputValueSats(dlcTxs)),
3823
+ },
3824
+ witnessScript: paymentVariant.redeem.output,
3825
+ inputSerialId: dlcClose.fundInputSerialId,
3826
+ });
3827
+
3828
+ // add all dlc close inputs
3829
+ dlcClose.fundingInputs.forEach((input) => {
3830
+ psbtInputs.push({
3831
+ hash: input.prevTx.txId.serialize(),
3832
+ index: input.prevTxVout,
3833
+ sequence: 0,
3834
+ witnessUtxo: {
3835
+ script: input.prevTx.outputs[input.prevTxVout].scriptPubKey
3836
+ .serialize()
3837
+ .slice(1),
3838
+ value: Number(input.prevTx.outputs[input.prevTxVout].value.sats),
3839
+ },
3840
+ inputSerialId: input.inputSerialId,
3841
+ });
3842
+ });
3843
+
3844
+ // sort all inputs in ascending order by serial ID
3845
+ // The only reason we are doing this is for privacy. If the fundingInput is
3846
+ // always first, it is very obvious. Hence, a serialId is randomly generated
3847
+ // and the inputs are sorted by that instead.
3848
+ const sortedPsbtInputs = psbtInputs.sort((a, b) =>
3849
+ Number(a.inputSerialId - b.inputSerialId),
3850
+ );
3851
+
3852
+ // Get index of fundingInput
3853
+ const fundingInputIndex = sortedPsbtInputs.findIndex(
3854
+ (input) => input.inputSerialId === dlcClose.fundInputSerialId,
3855
+ );
3856
+
3857
+ const offerFirst = dlcOffer.payoutSerialId < dlcAccept.payoutSerialId;
3858
+
3859
+ psbt.addOutput({
3860
+ value: Number(
3861
+ offerFirst
3862
+ ? dlcClose.offerPayoutSatoshis
3863
+ : dlcClose.acceptPayoutSatoshis,
3864
+ ),
3865
+ address: address.fromOutputScript(
3866
+ offerFirst ? dlcOffer.payoutSpk : dlcAccept.payoutSpk,
3867
+ network,
3868
+ ),
3869
+ });
3870
+
3871
+ psbt.addOutput({
3872
+ value: Number(
3873
+ offerFirst
3874
+ ? dlcClose.acceptPayoutSatoshis
3875
+ : dlcClose.offerPayoutSatoshis,
3876
+ ),
3877
+ address: address.fromOutputScript(
3878
+ offerFirst ? dlcAccept.payoutSpk : dlcOffer.payoutSpk,
3879
+ network,
3880
+ ),
3881
+ });
3882
+
3883
+ // add to psbt
3884
+ sortedPsbtInputs.forEach((input) => psbt.addInput(input));
3885
+
3886
+ const offerer = await this.isOfferer(dlcOffer, dlcAccept);
3887
+
3888
+ // Generate keypair to sign inputs
3889
+ const fundPrivateKeyPair = await this.GetFundKeyPair(
3890
+ dlcOffer,
3891
+ dlcAccept,
3892
+ offerer,
3893
+ );
3894
+
3895
+ // Sign dlc fundinginput
3896
+ psbt.signInput(fundingInputIndex, fundPrivateKeyPair);
3897
+
3898
+ const partialSig = [
3899
+ {
3900
+ pubkey: offerer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey,
3901
+ signature: script.signature.encode(dlcClose.closeSignature, 1), // encode using SIGHASH_ALL
3902
+ },
3903
+ ];
3904
+ psbt.updateInput(fundingInputIndex, { partialSig });
3905
+
3906
+ for (let i = 0; i < psbt.data.inputs.length; ++i) {
3907
+ if (i === fundingInputIndex) continue;
3908
+ if (!psbt.data.inputs[i].partialSig) psbt.data.inputs[i].partialSig = [];
3909
+
3910
+ const witnessI = dlcClose.fundingSignatures.witnessElements.findIndex(
3911
+ (el) =>
3912
+ Buffer.compare(
3913
+ Script.p2wpkhLock(hash160(el[1].witness)).serialize().subarray(1),
3914
+ psbt.data.inputs[i].witnessUtxo.script,
3915
+ ) === 0,
3916
+ );
3917
+
3918
+ const partialSig = [
3919
+ {
3920
+ pubkey:
3921
+ dlcClose.fundingSignatures.witnessElements[witnessI][1].witness,
3922
+ signature:
3923
+ dlcClose.fundingSignatures.witnessElements[witnessI][0].witness,
3924
+ },
3925
+ ];
3926
+
3927
+ psbt.updateInput(i, { partialSig });
3928
+ }
3929
+
3930
+ psbt.validateSignaturesOfAllInputs(
3931
+ (pubkey: Buffer, msghash: Buffer, signature: Buffer) => {
3932
+ return ecc.verify(msghash, pubkey, signature);
3933
+ },
3934
+ );
3935
+ psbt.finalizeAllInputs();
3936
+
3937
+ return psbt.extractTransaction().toHex();
3938
+ }
3939
+
3940
+ async fundingInputToInput(
3941
+ _input: FundingInput,
3942
+ findDerivationPath = true,
3943
+ ): Promise<Input> {
3944
+ assert(_input.type === MessageType.FundingInput, 'FundingInput must be V0');
3945
+ const network = await this.getConnectedNetwork();
3946
+ const input = _input as FundingInput;
3947
+ const prevTx = input.prevTx;
3948
+ const prevTxOut = prevTx.outputs[input.prevTxVout];
3949
+ const scriptPubKey = prevTxOut.scriptPubKey.serialize().subarray(1);
3950
+ const _address = address.fromOutputScript(scriptPubKey, network);
3951
+ let derivationPath: string;
3952
+
3953
+ if (findDerivationPath) {
3954
+ const inputAddress: Address = await this.client.wallet.findAddress([
3955
+ _address,
3956
+ ]);
3957
+ if (inputAddress) {
3958
+ derivationPath = inputAddress.derivationPath;
3959
+ }
3960
+ }
3961
+
3962
+ // Check if this FundingInput has DLC input information to preserve
3963
+ const dlcInputMessage = input.dlcInput;
3964
+
3965
+ let dlcInputInfo: DlcInputInfo | undefined;
3966
+ if (dlcInputMessage) {
3967
+ dlcInputInfo = {
3968
+ localFundPubkey: dlcInputMessage.localFundPubkey.toString('hex'),
3969
+ remoteFundPubkey: dlcInputMessage.remoteFundPubkey.toString('hex'),
3970
+ contractId: dlcInputMessage.contractId.toString('hex'),
3971
+ };
3972
+ }
3973
+
3974
+ return new Input(
3975
+ prevTx.txId.toString(),
3976
+ input.prevTxVout,
3977
+ _address,
3978
+ prevTxOut.value.bitcoin,
3979
+ Number(prevTxOut.value.sats),
3980
+ derivationPath,
3981
+ input.maxWitnessLen,
3982
+ input.redeemScript ? input.redeemScript.toString('hex') : '',
3983
+ input.inputSerialId,
3984
+ scriptPubKey.toString('hex'),
3985
+ undefined, // label
3986
+ undefined, // confirmations
3987
+ undefined, // spendable
3988
+ undefined, // solvable
3989
+ undefined, // safe
3990
+ dlcInputInfo, // Preserve DLC input information if present
3991
+ );
3992
+ }
3993
+
3994
+ async inputToFundingInput(input: Input): Promise<FundingInput> {
3995
+ const fundingInput = new FundingInput();
3996
+ fundingInput.prevTxVout = input.vout;
3997
+
3998
+ let txRaw = '';
3999
+ try {
4000
+ txRaw = await this.getMethod('getRawTransactionByHash')(input.txid);
4001
+ } catch {
4002
+ try {
4003
+ txRaw = (await this.getMethod('jsonrpc')('gettransaction', input.txid))
4004
+ .hex;
4005
+ } catch {
4006
+ throw Error(
4007
+ `Cannot find tx ${input.txid} in inputToFundingInput using getrawtransactionbyhash or gettransaction`,
4008
+ );
4009
+ }
4010
+ }
4011
+
4012
+ const tx = Tx.decode(StreamReader.fromHex(txRaw));
4013
+
4014
+ fundingInput.prevTx = tx;
4015
+ fundingInput.sequence = Sequence.default();
4016
+ fundingInput.maxWitnessLen = input.maxWitnessLength
4017
+ ? input.maxWitnessLength
4018
+ : 108;
4019
+ fundingInput.redeemScript = input.redeemScript
4020
+ ? Buffer.from(input.redeemScript, 'hex')
4021
+ : Buffer.from('', 'hex');
4022
+ fundingInput.inputSerialId = input.inputSerialId
4023
+ ? input.inputSerialId
4024
+ : generateSerialId();
4025
+
4026
+ // Preserve DLC input information if present
4027
+ if (input.isDlcInput()) {
4028
+ const dlcInputInfo = input.dlcInput!;
4029
+ const dlcInput = new DlcInput();
4030
+ dlcInput.localFundPubkey = Buffer.from(
4031
+ dlcInputInfo.localFundPubkey,
4032
+ 'hex',
4033
+ );
4034
+ dlcInput.remoteFundPubkey = Buffer.from(
4035
+ dlcInputInfo.remoteFundPubkey,
4036
+ 'hex',
4037
+ );
4038
+ dlcInput.contractId = Buffer.alloc(32); // Placeholder contract ID
4039
+
4040
+ fundingInput.dlcInput = dlcInput;
4041
+ }
4042
+
4043
+ return fundingInput;
4044
+ }
4045
+
4046
+ async getConnectedNetwork(): Promise<BitcoinNetwork> {
4047
+ return this._network;
4048
+ }
4049
+
4050
+ /**
4051
+ * Convert BitcoinNetwork to bitcoinjs-lib network format
4052
+ */
4053
+ private getBitcoinJsNetwork(): any {
4054
+ const network = this._network;
4055
+ if (network.name === 'bitcoin') {
4056
+ return networks.bitcoin;
4057
+ } else if (network.name === 'testnet') {
4058
+ return networks.testnet;
4059
+ } else if (network.name === 'regtest') {
4060
+ return networks.regtest;
4061
+ } else {
4062
+ // Default to mainnet if unknown
4063
+ return networks.bitcoin;
4064
+ }
4065
+ }
4066
+
4067
+ /**
4068
+ * Calculate the maximum collateral possible with given inputs
4069
+ * @param inputs Array of Input objects to use for funding
4070
+ * @param feeRatePerVb Fee rate in satoshis per virtual byte
4071
+ * @param contractCount Number of DLC contracts (default: 1)
4072
+ * @returns Maximum collateral amount in satoshis
4073
+ */
4074
+ async calculateMaxCollateral(
4075
+ inputs: Input[],
4076
+ feeRatePerVb: bigint,
4077
+ contractCount: number = 1,
4078
+ ): Promise<bigint> {
4079
+ if (inputs.length === 0) {
4080
+ return BigInt(0);
4081
+ }
4082
+
4083
+ try {
4084
+ // Convert Input[] to FundingInput[]
4085
+ const fundingInputs = await Promise.all(
4086
+ inputs.map((input) => this.inputToFundingInput(input)),
4087
+ );
4088
+
4089
+ // Use node-dlc's calculateMaxCollateral function
4090
+ // For single-funded DLC, pass only offerer inputs and fee rate
4091
+ return BatchDlcTxBuilder.calculateMaxCollateral(
4092
+ fundingInputs,
4093
+ feeRatePerVb,
4094
+ contractCount,
4095
+ );
4096
+ } catch (error) {
4097
+ // If calculation fails, return 0 to indicate insufficient funds
4098
+ console.warn('calculateMaxCollateral failed:', error);
4099
+ return BigInt(0);
4100
+ }
4101
+ }
4102
+
4103
+ /**
4104
+ * Create a funding input with DLC input information for splicing
4105
+ * @param dlcInputInfo DLC input information
4106
+ * @param fundingTxHex Raw transaction hex of the funding transaction
4107
+ */
4108
+ async createDlcFundingInput(
4109
+ dlcInputInfo: DlcInputInfoRequest,
4110
+ fundingTxHex: string,
4111
+ ): Promise<FundingInput> {
4112
+ const fundingInput = new FundingInput();
4113
+ const tx = Tx.decode(StreamReader.fromHex(fundingTxHex));
4114
+
4115
+ fundingInput.prevTx = tx;
4116
+ fundingInput.prevTxVout = dlcInputInfo.fundVout;
4117
+ fundingInput.sequence = Sequence.default();
4118
+ fundingInput.maxWitnessLen = dlcInputInfo.maxWitnessLength || 220;
4119
+ fundingInput.redeemScript = Buffer.from('', 'hex'); // Empty for P2WSH
4120
+ fundingInput.inputSerialId = BigInt(
4121
+ dlcInputInfo.inputSerialId || generateSerialId(),
4122
+ );
4123
+
4124
+ // Create the DLC multisig script for address generation
4125
+ const localPubkey = Buffer.from(dlcInputInfo.localFundPubkey, 'hex');
4126
+ const remotePubkey = Buffer.from(dlcInputInfo.remoteFundPubkey, 'hex');
4127
+
4128
+ // Use the same deterministic ordering as cfd-dlc-js: lexicographic by hex
4129
+ // This matches GetOrderedPubkeys() in cfddlc_transactions.cpp
4130
+ const orderedPubkeys =
4131
+ dlcInputInfo.localFundPubkey < dlcInputInfo.remoteFundPubkey
4132
+ ? [localPubkey, remotePubkey]
4133
+ : [remotePubkey, localPubkey];
4134
+
4135
+ const network = await this.getConnectedNetwork();
4136
+
4137
+ // Create 2-of-2 multisig payment using deterministic ordering
4138
+ const p2ms = payments.p2ms({
4139
+ m: 2,
4140
+ pubkeys: orderedPubkeys,
4141
+ network,
4142
+ });
4143
+
4144
+ const paymentVariant = payments.p2wsh({
4145
+ redeem: p2ms,
4146
+ network,
4147
+ });
4148
+
4149
+ const multisigAddress = paymentVariant.address!;
4150
+
4151
+ // Verify this matches the actual funding output address
4152
+ const actualFundingOutput = tx.outputs[dlcInputInfo.fundVout];
4153
+ const actualFundingAddress = address.fromOutputScript(
4154
+ actualFundingOutput.scriptPubKey.serialize().subarray(1),
4155
+ network,
4156
+ );
4157
+
4158
+ if (actualFundingAddress !== multisigAddress) {
4159
+ throw new Error(
4160
+ `DLC funding address mismatch. ` +
4161
+ `Expected: ${actualFundingAddress}, ` +
4162
+ `Constructed: ${multisigAddress}`,
4163
+ );
4164
+ }
4165
+
4166
+ // Add toUtxo method that's expected by GetInputsForAmount
4167
+ (fundingInput as FundingInput & { toUtxo: () => Utxo }).toUtxo = () => {
4168
+ return new Utxo(
4169
+ dlcInputInfo.fundTxid,
4170
+ dlcInputInfo.fundVout,
4171
+ Amount.FromSatoshis(Number(dlcInputInfo.fundAmount)),
4172
+ multisigAddress,
4173
+ dlcInputInfo.maxWitnessLength || 220,
4174
+ undefined, // DLC inputs don't have derivation paths
4175
+ fundingInput.inputSerialId,
4176
+ );
4177
+ };
4178
+
4179
+ // Create proper DlcInput object for splicing detection and signing
4180
+ const dlcInput = new DlcInput();
4181
+ dlcInput.localFundPubkey = Buffer.from(dlcInputInfo.localFundPubkey, 'hex');
4182
+ dlcInput.remoteFundPubkey = Buffer.from(
4183
+ dlcInputInfo.remoteFundPubkey,
4184
+ 'hex',
4185
+ );
4186
+ dlcInput.contractId = Buffer.from(dlcInputInfo.contractId, 'hex');
4187
+
4188
+ fundingInput.dlcInput = dlcInput;
4189
+
4190
+ return fundingInput;
4191
+ }
4192
+ }
4193
+
4194
+ export interface BasicInitializeResponse {
4195
+ fundingPubKey: Buffer;
4196
+ payoutSPK: Buffer;
4197
+ payoutSerialId: bigint;
4198
+ changeSPK: Buffer;
4199
+ changeSerialId: bigint;
4200
+ }
4201
+
4202
+ export interface InitializeResponse extends BasicInitializeResponse {
4203
+ fundingInputs: FundingInput[];
4204
+ }
4205
+
4206
+ export interface BatchBaseInitializeResponse {
4207
+ fundingPubKey: Buffer;
4208
+ payoutSPK: Buffer;
4209
+ payoutSerialId: bigint;
4210
+ }
4211
+
4212
+ export interface BatchInitializeResponse {
4213
+ initializeResponses: BatchBaseInitializeResponse[];
4214
+ fundingInputs: FundingInput[];
4215
+ changeSPK: Buffer;
4216
+ changeSerialId: bigint;
4217
+ }
4218
+
4219
+ export interface AcceptDlcOfferResponse {
4220
+ dlcAccept: DlcAccept;
4221
+ dlcTransactions: DlcTransactions;
4222
+ }
4223
+
4224
+ export interface BatchAcceptDlcOfferResponse {
4225
+ dlcAccepts: DlcAccept[];
4226
+ dlcTransactionsList: DlcTransactions[];
4227
+ }
4228
+
4229
+ export interface SignDlcAcceptResponse {
4230
+ dlcSign: DlcSign;
4231
+ dlcTransactions: DlcTransactions;
4232
+ }
4233
+
4234
+ export interface BatchSignDlcAcceptResponse {
4235
+ dlcSigns: DlcSign[];
4236
+ dlcTransactionsList: DlcTransactions[];
4237
+ }
4238
+
4239
+ export interface GetPayoutsResponse {
4240
+ payouts: PayoutRequest[];
4241
+ payoutGroups: PayoutGroup[];
4242
+ messagesList: Messages[];
4243
+ }
4244
+
4245
+ export interface CreateDlcTxsResponse {
4246
+ dlcTransactions: DlcTransactions;
4247
+ messagesList: Messages[];
4248
+ }
4249
+
4250
+ export interface CreateBatchDlcTxsResponse {
4251
+ dlcTransactionsList: DlcTransactions[];
4252
+ nestedMessagesList: Messages[][];
4253
+ }
4254
+
4255
+ interface ISig {
4256
+ encryptedSig: Buffer;
4257
+ dleqProof: Buffer;
4258
+ }
4259
+
4260
+ export interface CreateCetAdaptorAndRefundSigsResponse {
4261
+ cetSignatures: CetAdaptorSignatures;
4262
+ refundSignature: Buffer;
4263
+ }
4264
+
4265
+ interface PayoutGroup {
4266
+ payout: bigint;
4267
+ groups: number[][];
4268
+ }
4269
+
4270
+ interface FindOutcomeResponse {
4271
+ index: number;
4272
+ groupLength: number;
4273
+ }
4274
+
4275
+ export interface Change {
4276
+ value: number;
4277
+ }
4278
+
4279
+ export interface Output {
4280
+ value: number;
4281
+ id?: string;
4282
+ }
4283
+
4284
+ export interface InputsForAmountResponse {
4285
+ inputs: Input[];
4286
+ change: Change;
4287
+ outputs: Output[];
4288
+ fee: number;
4289
+ }
4290
+
4291
+ export interface InputsForDualAmountResponse {
4292
+ inputs: Input[];
4293
+ fee: number;
4294
+ }