@atomicfinance/bitcoin-ddk-provider 4.1.13 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -85,7 +85,20 @@ import {
85
85
  import crypto from 'crypto';
86
86
  import { ECPairFactory, ECPairInterface } from 'ecpair';
87
87
 
88
- import { checkTypes, generateSerialId, outputsToPayouts } from './utils/Utils';
88
+ import {
89
+ checkTypes,
90
+ computeContractId,
91
+ createP2MSMultisig,
92
+ createP2WSHMultisig,
93
+ createP2WSHMultisigFromOrdered,
94
+ ensureBuffer,
95
+ ensureCompactSignature,
96
+ ensureDerSignature,
97
+ generateSerialId,
98
+ orderPubkeysLexicographically,
99
+ outputsToPayouts,
100
+ sortFundingInputsBySerialId,
101
+ } from './utils/Utils';
89
102
 
90
103
  const ECPair = ECPairFactory(ecc);
91
104
 
@@ -105,220 +118,6 @@ export default class BitcoinDdkProvider extends Provider {
105
118
  }
106
119
  }
107
120
 
108
- /**
109
- * Helper function to ensure we have a Buffer object
110
- * Handles cases where Buffer objects have been serialized/deserialized
111
- */
112
- private ensureBuffer(
113
- bufferLike: Buffer | { type: string; data: number[] } | any,
114
- ): Buffer {
115
- if (Buffer.isBuffer(bufferLike)) {
116
- return bufferLike;
117
- }
118
- if (bufferLike && bufferLike.type === 'Buffer' && bufferLike.data) {
119
- return Buffer.from(bufferLike.data);
120
- }
121
- return bufferLike;
122
- }
123
-
124
- /**
125
- * Detect if signature is in compact format (64 bytes) or DER format
126
- * and convert compact to DER if needed, adding SIGHASH_ALL flag
127
- */
128
- private ensureDerSignature(signature: Buffer): Buffer {
129
- // If signature is 64 bytes, it's likely compact format (32-byte r + 32-byte s)
130
- if (signature.length === 64) {
131
- // Convert compact signature to DER format
132
- const r = signature.slice(0, 32);
133
- const s = signature.slice(32, 64);
134
-
135
- // Create DER encoding manually
136
- // DER format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S]
137
-
138
- // Remove leading zeros from r and s, but keep at least one byte
139
- let rBytes = r;
140
- while (
141
- rBytes.length > 1 &&
142
- rBytes[0] === 0x00 &&
143
- (rBytes[1] & 0x80) === 0
144
- ) {
145
- rBytes = rBytes.slice(1);
146
- }
147
-
148
- let sBytes = s;
149
- while (
150
- sBytes.length > 1 &&
151
- sBytes[0] === 0x00 &&
152
- (sBytes[1] & 0x80) === 0
153
- ) {
154
- sBytes = sBytes.slice(1);
155
- }
156
-
157
- // Add padding byte if high bit is set (to keep numbers positive)
158
- if ((rBytes[0] & 0x80) !== 0) {
159
- rBytes = Buffer.concat([Buffer.from([0x00]), rBytes]);
160
- }
161
- if ((sBytes[0] & 0x80) !== 0) {
162
- sBytes = Buffer.concat([Buffer.from([0x00]), sBytes]);
163
- }
164
-
165
- const totalLength = 2 + rBytes.length + 2 + sBytes.length;
166
-
167
- const derSignature = Buffer.concat([
168
- Buffer.from([0x30, totalLength]), // SEQUENCE tag and total length
169
- Buffer.from([0x02, rBytes.length]), // INTEGER tag and R length
170
- rBytes,
171
- Buffer.from([0x02, sBytes.length]), // INTEGER tag and S length
172
- sBytes,
173
- Buffer.from([0x01]), // SIGHASH_ALL flag
174
- ]);
175
-
176
- return derSignature;
177
- }
178
-
179
- // If it's already DER format, check if it has SIGHASH flag
180
- if (signature.length > 0 && signature[0] === 0x30) {
181
- // Check if it already has a SIGHASH flag (last byte should be 0x01 for SIGHASH_ALL)
182
- if (signature[signature.length - 1] !== 0x01) {
183
- // Add SIGHASH_ALL flag
184
- return Buffer.concat([signature, Buffer.from([0x01])]);
185
- }
186
- return signature;
187
- }
188
-
189
- // For other formats, return as-is
190
- return signature;
191
- }
192
-
193
- /**
194
- * Detect if signature is in DER format and convert to compact format (64 bytes)
195
- * by extracting r and s values, removing SIGHASH flag if present
196
- */
197
- private ensureCompactSignature(signature: Buffer): Buffer {
198
- // If signature is already 64 bytes, it's likely already compact format
199
- if (signature.length === 64) {
200
- return signature;
201
- }
202
-
203
- // Check if it's DER format (starts with 0x30)
204
- if (signature.length > 6 && signature[0] === 0x30) {
205
- let derSig = signature;
206
-
207
- // Remove SIGHASH flag if present (last byte is typically 0x01 for SIGHASH_ALL)
208
- if (signature[signature.length - 1] === 0x01) {
209
- derSig = signature.slice(0, -1);
210
- }
211
-
212
- // Parse DER format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S]
213
- if (derSig[0] !== 0x30) {
214
- throw new Error('Invalid DER signature: missing SEQUENCE tag');
215
- }
216
-
217
- const totalLength = derSig[1];
218
- if (derSig.length < totalLength + 2) {
219
- throw new Error('Invalid DER signature: length mismatch');
220
- }
221
-
222
- let offset = 2;
223
-
224
- // Parse R value
225
- if (derSig[offset] !== 0x02) {
226
- throw new Error('Invalid DER signature: missing INTEGER tag for R');
227
- }
228
- offset++;
229
-
230
- const rLength = derSig[offset];
231
- offset++;
232
-
233
- if (offset + rLength > derSig.length) {
234
- throw new Error(
235
- 'Invalid DER signature: R length exceeds signature length',
236
- );
237
- }
238
-
239
- let rBytes = derSig.slice(offset, offset + rLength);
240
- offset += rLength;
241
-
242
- // Parse S value
243
- if (derSig[offset] !== 0x02) {
244
- throw new Error('Invalid DER signature: missing INTEGER tag for S');
245
- }
246
- offset++;
247
-
248
- const sLength = derSig[offset];
249
- offset++;
250
-
251
- if (offset + sLength > derSig.length) {
252
- throw new Error(
253
- 'Invalid DER signature: S length exceeds signature length',
254
- );
255
- }
256
-
257
- let sBytes = derSig.slice(offset, offset + sLength);
258
-
259
- // Remove leading zero padding from r and s (DER may pad to prevent negative interpretation)
260
- while (rBytes.length > 1 && rBytes[0] === 0x00) {
261
- rBytes = rBytes.slice(1);
262
- }
263
- while (sBytes.length > 1 && sBytes[0] === 0x00) {
264
- sBytes = sBytes.slice(1);
265
- }
266
-
267
- // Pad to 32 bytes each (compact format requires exactly 32 bytes for r and s)
268
- while (rBytes.length < 32) {
269
- rBytes = Buffer.concat([Buffer.from([0x00]), rBytes]);
270
- }
271
- while (sBytes.length < 32) {
272
- sBytes = Buffer.concat([Buffer.from([0x00]), sBytes]);
273
- }
274
-
275
- if (rBytes.length !== 32 || sBytes.length !== 32) {
276
- throw new Error('Invalid signature values: r or s exceeds 32 bytes');
277
- }
278
-
279
- // Combine r and s into 64-byte compact format
280
- return Buffer.concat([rBytes, sBytes]);
281
- }
282
-
283
- // For other formats, throw error as we can't convert
284
- throw new Error(
285
- 'Unable to convert signature to compact format: unknown format',
286
- );
287
- }
288
-
289
- /**
290
- * Compute contract ID from fund transaction ID, output index, and temporary contract ID
291
- * Matches the Rust implementation in rust-dlc
292
- */
293
- private computeContractId(
294
- fundTxId: Buffer,
295
- fundOutputIndex: number,
296
- temporaryContractId: Buffer,
297
- ): Buffer {
298
- if (fundTxId.length !== 32) {
299
- throw new Error('Fund transaction ID must be 32 bytes');
300
- }
301
- if (temporaryContractId.length !== 32) {
302
- throw new Error('Temporary contract ID must be 32 bytes');
303
- }
304
- if (fundOutputIndex > 0xffff) {
305
- throw new Error('Fund output index must fit in 16 bits');
306
- }
307
-
308
- const result = Buffer.alloc(32);
309
-
310
- // XOR fund_tx_id with temporary_id, with byte order reversal for fund_tx_id
311
- for (let i = 0; i < 32; i++) {
312
- result[i] = fundTxId[31 - i] ^ temporaryContractId[i];
313
- }
314
-
315
- // XOR the fund output index into the last two bytes
316
- result[30] ^= (fundOutputIndex >> 8) & 0xff; // High byte
317
- result[31] ^= fundOutputIndex & 0xff; // Low byte
318
-
319
- return result;
320
- }
321
-
322
121
  /**
323
122
  * Create refund signature using PSBT method instead of DDK
324
123
  */
@@ -338,22 +137,11 @@ export default class BitcoinDdkProvider extends Provider {
338
137
  );
339
138
  }
340
139
 
341
- // Create the same funding script as createDlcClose
342
- const fundingPubKeys =
343
- Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
344
- ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
345
- : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
346
-
347
- const p2ms = payments.p2ms({
348
- m: 2,
349
- pubkeys: fundingPubKeys,
140
+ const paymentVariant = createP2WSHMultisig(
141
+ dlcOffer.fundingPubkey,
142
+ dlcAccept.fundingPubkey,
350
143
  network,
351
- });
352
-
353
- const paymentVariant = payments.p2wsh({
354
- redeem: p2ms,
355
- network,
356
- });
144
+ );
357
145
 
358
146
  // Add the funding input with sequence from refund transaction
359
147
  psbt.addInput({
@@ -404,7 +192,7 @@ export default class BitcoinDdkProvider extends Provider {
404
192
  const derSignature = partialSig.signature;
405
193
 
406
194
  // Convert DER signature to compact format
407
- return this.ensureCompactSignature(derSignature);
195
+ return ensureCompactSignature(derSignature);
408
196
  }
409
197
 
410
198
  /**
@@ -962,12 +750,19 @@ export default class BitcoinDdkProvider extends Provider {
962
750
  }),
963
751
  );
964
752
 
965
- // Calculate input amounts
966
- const localInputAmount = localRegularInputs.reduce<number>(
753
+ // Calculate input amounts (including both regular inputs and DLC inputs)
754
+ const localRegularInputAmount = localRegularInputs.reduce<number>(
967
755
  (prev, cur) => prev + cur.amount.GetSatoshiAmount(),
968
756
  0,
969
757
  );
970
758
 
759
+ const localDlcInputAmount = localDlcInputs.reduce<number>(
760
+ (prev, cur) => prev + Number(cur.fundAmount),
761
+ 0,
762
+ );
763
+
764
+ const localInputAmount = localRegularInputAmount + localDlcInputAmount;
765
+
971
766
  const remoteInputAmount = remoteInputs.reduce<number>(
972
767
  (prev, cur) => prev + cur.amount.GetSatoshiAmount(),
973
768
  0,
@@ -1046,7 +841,7 @@ export default class BitcoinDdkProvider extends Provider {
1046
841
  BigInt(dlcOffer.feeRatePerVb),
1047
842
  0,
1048
843
  dlcOffer.cetLocktime,
1049
- dlcOffer.fundOutputSerialId,
844
+ BigInt(dlcOffer.fundOutputSerialId),
1050
845
  );
1051
846
  } else {
1052
847
  // Use regular DLC transactions when no DLC inputs
@@ -1280,19 +1075,12 @@ export default class BitcoinDdkProvider extends Provider {
1280
1075
 
1281
1076
  const cetsHex = dlcTxs.cets.map((cet) => cet.serialize().toString('hex'));
1282
1077
 
1283
- // Create the correct P2WSH multisig funding script
1284
- const fundingPubKeys =
1285
- Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
1286
- ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
1287
- : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
1288
-
1289
- const p2ms = payments.p2ms({
1290
- m: 2,
1291
- pubkeys: fundingPubKeys,
1292
- network,
1293
- });
1294
-
1295
1078
  // We need the redeem script (multisig), not the P2WSH output
1079
+ const p2ms = createP2MSMultisig(
1080
+ dlcOffer.fundingPubkey,
1081
+ dlcAccept.fundingPubkey,
1082
+ network,
1083
+ );
1296
1084
  const fundingSPK = p2ms.output!;
1297
1085
 
1298
1086
  // For finding the private key, we still need the individual P2WPKH address
@@ -1499,16 +1287,12 @@ export default class BitcoinDdkProvider extends Provider {
1499
1287
 
1500
1288
  // Create the correct P2WSH multisig funding script for verification
1501
1289
  const network = await this.getConnectedNetwork();
1502
- const verifyFundingPubKeys =
1503
- Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
1504
- ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
1505
- : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
1506
-
1507
- const verifyP2ms = payments.p2ms({
1508
- m: 2,
1509
- pubkeys: verifyFundingPubKeys,
1290
+
1291
+ const verifyP2ms = createP2MSMultisig(
1292
+ dlcOffer.fundingPubkey,
1293
+ dlcAccept.fundingPubkey,
1510
1294
  network,
1511
- });
1295
+ );
1512
1296
 
1513
1297
  // We need the redeem script (multisig), not the P2WSH output
1514
1298
  const fundingSPK = verifyP2ms.output!;
@@ -1658,7 +1442,7 @@ export default class BitcoinDdkProvider extends Provider {
1658
1442
  }
1659
1443
  }
1660
1444
 
1661
- private async CreateFundingSigsAlt(
1445
+ private async CreateFundingSigs(
1662
1446
  dlcOffer: DlcOffer,
1663
1447
  dlcAccept: DlcAccept,
1664
1448
  dlcTxs: DlcTransactions,
@@ -1670,14 +1454,10 @@ export default class BitcoinDdkProvider extends Provider {
1670
1454
  const network = await this.getConnectedNetwork();
1671
1455
  const psbt = new Psbt({ network });
1672
1456
 
1673
- // Combine all funding inputs from both parties
1674
- const allFundingInputs = [
1457
+ const allFundingInputs = sortFundingInputsBySerialId([
1675
1458
  ...dlcOffer.fundingInputs,
1676
1459
  ...dlcAccept.fundingInputs,
1677
- ];
1678
-
1679
- // Sort by inputSerialId to reconstruct proper transaction order
1680
- allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
1460
+ ]);
1681
1461
 
1682
1462
  // Add all inputs to PSBT with proper witnessUtxo
1683
1463
  for (const fundingInput of allFundingInputs) {
@@ -1756,11 +1536,42 @@ export default class BitcoinDdkProvider extends Provider {
1756
1536
 
1757
1537
  // Check if this is a DLC input (2-of-2 multisig from previous DLC)
1758
1538
  if (fundingInput.dlcInput) {
1759
- // For DLC inputs, we'll need to handle 2-of-2 multisig signing differently
1760
- // For now, throw an error as this requires implementing multisig support
1761
- throw new Error(
1762
- 'DLC input signing not yet implemented in CreateFundingSigsAlt',
1539
+ const paymentVariant = createP2WSHMultisig(
1540
+ fundingInput.dlcInput.localFundPubkey,
1541
+ fundingInput.dlcInput.remoteFundPubkey,
1542
+ network,
1763
1543
  );
1544
+
1545
+ const redeemScript = paymentVariant.redeem.output;
1546
+
1547
+ // Find the correct private key for this DLC input
1548
+ const dlcPrivKey = await this.findDlcFundingPrivateKey(
1549
+ fundingInput.dlcInput.localFundPubkey.toString('hex'),
1550
+ fundingInput.dlcInput.remoteFundPubkey.toString('hex'),
1551
+ );
1552
+ const keyPair = ECPair.fromPrivateKey(Buffer.from(dlcPrivKey, 'hex'));
1553
+
1554
+ // Update the input with the witnessScript before signing
1555
+ psbt.updateInput(inputIndex, {
1556
+ witnessScript: redeemScript,
1557
+ });
1558
+
1559
+ psbt.signInput(inputIndex, keyPair);
1560
+
1561
+ const inputData = psbt.data.inputs[inputIndex];
1562
+ const partialSigs = inputData.partialSig;
1563
+ if (!partialSigs || partialSigs.length === 0) {
1564
+ throw new Error(
1565
+ `No signatures found for input ${inputIndex} after signing`,
1566
+ );
1567
+ }
1568
+
1569
+ const sigWitness = new ScriptWitnessV0();
1570
+ sigWitness.witness = ensureBuffer(partialSigs[0].signature);
1571
+ const pubKeyWitness = new ScriptWitnessV0();
1572
+ pubKeyWitness.witness = keyPair.publicKey;
1573
+
1574
+ witnessElements.push([sigWitness, pubKeyWitness]);
1764
1575
  } else {
1765
1576
  // For P2WPKH inputs, use PSBT signing
1766
1577
  psbt.signInput(inputIndex, keyPair);
@@ -1776,7 +1587,7 @@ export default class BitcoinDdkProvider extends Provider {
1776
1587
 
1777
1588
  // For P2WPKH, create witness manually: [signature, publicKey]
1778
1589
  const sigWitness = new ScriptWitnessV0();
1779
- sigWitness.witness = this.ensureBuffer(partialSigs[0].signature);
1590
+ sigWitness.witness = ensureBuffer(partialSigs[0].signature);
1780
1591
  const pubKeyWitness = new ScriptWitnessV0();
1781
1592
  pubKeyWitness.witness = keyPair.publicKey;
1782
1593
 
@@ -1792,7 +1603,7 @@ export default class BitcoinDdkProvider extends Provider {
1792
1603
  return fundingSignatures;
1793
1604
  }
1794
1605
 
1795
- private async VerifyFundingSigsAlt(
1606
+ private async VerifyFundingSigs(
1796
1607
  dlcOffer: DlcOffer,
1797
1608
  dlcAccept: DlcAccept,
1798
1609
  dlcSign: DlcSign,
@@ -1811,12 +1622,10 @@ export default class BitcoinDdkProvider extends Provider {
1811
1622
  ? dlcAccept.fundingInputs
1812
1623
  : dlcOffer.fundingInputs;
1813
1624
 
1814
- // Combine all inputs to get the correct ordering
1815
- const allFundingInputs = [
1625
+ const allFundingInputs = sortFundingInputsBySerialId([
1816
1626
  ...dlcOffer.fundingInputs,
1817
1627
  ...dlcAccept.fundingInputs,
1818
- ];
1819
- allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
1628
+ ]);
1820
1629
 
1821
1630
  // Compare transaction IDs
1822
1631
  const dlcFundTxId = dlcTxs.fundTx.txId.toString();
@@ -1870,8 +1679,61 @@ export default class BitcoinDdkProvider extends Provider {
1870
1679
  // Add the funding signatures to the PSBT as partial signatures
1871
1680
  let witnessIndex = 0;
1872
1681
  for (const fundingInput of signingPartyInputs) {
1873
- // Skip DLC inputs for now (same as CreateFundingSigsAlt)
1682
+ // Handle DLC inputs with multisig verification
1874
1683
  if (fundingInput.dlcInput) {
1684
+ const dlcInput = fundingInput.dlcInput;
1685
+
1686
+ const paymentVariant = createP2WSHMultisig(
1687
+ dlcInput.localFundPubkey,
1688
+ dlcInput.remoteFundPubkey,
1689
+ network,
1690
+ );
1691
+
1692
+ // Find this input's index in the sorted transaction
1693
+ const inputIndex = allFundingInputs.findIndex(
1694
+ (input) =>
1695
+ input.prevTx.txId.toString() ===
1696
+ fundingInput.prevTx.txId.toString() &&
1697
+ input.prevTxVout === fundingInput.prevTxVout,
1698
+ );
1699
+
1700
+ if (inputIndex === -1) {
1701
+ throw new Error(
1702
+ `DLC input not found in transaction: ${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`,
1703
+ );
1704
+ }
1705
+
1706
+ // Update the input with the witnessScript for verification
1707
+ psbt.updateInput(inputIndex, {
1708
+ witnessScript: paymentVariant.redeem.output,
1709
+ });
1710
+
1711
+ // Add the DLC signature from witness elements if available
1712
+ if (witnessIndex < dlcSign.fundingSignatures.witnessElements.length) {
1713
+ const witnessElement =
1714
+ dlcSign.fundingSignatures.witnessElements[witnessIndex];
1715
+ const signature = witnessElement[0].witness;
1716
+ const publicKey = witnessElement[1].witness;
1717
+
1718
+ psbt.updateInput(inputIndex, {
1719
+ partialSig: [
1720
+ {
1721
+ pubkey: publicKey,
1722
+ signature: ensureDerSignature(signature),
1723
+ },
1724
+ ],
1725
+ });
1726
+
1727
+ // Verify the signature for this DLC input
1728
+ psbt.validateSignaturesOfInput(
1729
+ inputIndex,
1730
+ (pubkey: Buffer, msghash: Buffer, signature: Buffer) => {
1731
+ return ecc.verify(msghash, pubkey, signature);
1732
+ },
1733
+ );
1734
+
1735
+ witnessIndex++;
1736
+ }
1875
1737
  continue;
1876
1738
  }
1877
1739
 
@@ -1904,7 +1766,7 @@ export default class BitcoinDdkProvider extends Provider {
1904
1766
  partialSig: [
1905
1767
  {
1906
1768
  pubkey: publicKey,
1907
- signature: this.ensureDerSignature(signature),
1769
+ signature: ensureDerSignature(signature),
1908
1770
  },
1909
1771
  ],
1910
1772
  });
@@ -1981,12 +1843,10 @@ export default class BitcoinDdkProvider extends Provider {
1981
1843
  Buffer.from(dlcTxs.fundTx.serialize()),
1982
1844
  );
1983
1845
 
1984
- // Combine all inputs to get the correct ordering
1985
- const allFundingInputs = [
1846
+ const allFundingInputs = sortFundingInputsBySerialId([
1986
1847
  ...dlcOffer.fundingInputs,
1987
1848
  ...dlcAccept.fundingInputs,
1988
- ];
1989
- allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
1849
+ ]);
1990
1850
 
1991
1851
  // Calculate detailed sighash information
1992
1852
  const details = [];
@@ -2047,7 +1907,7 @@ export default class BitcoinDdkProvider extends Provider {
2047
1907
  : dlcSign.refundSignature;
2048
1908
 
2049
1909
  // Ensure signature is in DER format (convert from compact if needed)
2050
- const refundSignature = this.ensureDerSignature(rawRefundSignature);
1910
+ const refundSignature = ensureDerSignature(rawRefundSignature);
2051
1911
 
2052
1912
  const signingPubkey = isOfferer
2053
1913
  ? dlcAccept.fundingPubkey
@@ -2063,22 +1923,11 @@ export default class BitcoinDdkProvider extends Provider {
2063
1923
  // Create a PSBT for the refund transaction verification using same approach as createDlcClose
2064
1924
  const psbt = new Psbt({ network });
2065
1925
 
2066
- // Create the same funding script as createDlcClose
2067
- const fundingPubKeys =
2068
- Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
2069
- ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
2070
- : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
2071
-
2072
- const p2ms = payments.p2ms({
2073
- m: 2,
2074
- pubkeys: fundingPubKeys,
2075
- network,
2076
- });
2077
-
2078
- const paymentVariant = payments.p2wsh({
2079
- redeem: p2ms,
1926
+ const paymentVariant = createP2WSHMultisig(
1927
+ dlcOffer.fundingPubkey,
1928
+ dlcAccept.fundingPubkey,
2080
1929
  network,
2081
- });
1930
+ );
2082
1931
 
2083
1932
  // Add the funding input with sequence from refund transaction
2084
1933
  psbt.addInput({
@@ -2132,23 +1981,11 @@ export default class BitcoinDdkProvider extends Provider {
2132
1981
  ): Buffer {
2133
1982
  const network = this.getBitcoinJsNetwork();
2134
1983
 
2135
- // Sort funding pubkeys in lexicographical order as per DLC spec
2136
- const fundingPubKeys =
2137
- Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
2138
- ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
2139
- : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
2140
-
2141
- // Create 2-of-2 multisig script
2142
- const p2ms = payments.p2ms({
2143
- m: 2,
2144
- pubkeys: fundingPubKeys,
1984
+ const paymentVariant = createP2WSHMultisig(
1985
+ dlcOffer.fundingPubkey,
1986
+ dlcAccept.fundingPubkey,
2145
1987
  network,
2146
- });
2147
-
2148
- const paymentVariant = payments.p2wsh({
2149
- redeem: p2ms,
2150
- network,
2151
- });
1988
+ );
2152
1989
 
2153
1990
  return paymentVariant.redeem!.output!;
2154
1991
  }
@@ -2163,27 +2000,20 @@ export default class BitcoinDdkProvider extends Provider {
2163
2000
  const network = await this.getConnectedNetwork();
2164
2001
  const psbt = new Psbt({ network });
2165
2002
 
2166
- // Combine and sort all funding inputs by serial ID (same as CreateFundingSigsAlt)
2167
- const allFundingInputs = [
2003
+ const allFundingInputs = sortFundingInputsBySerialId([
2168
2004
  ...dlcOffer.fundingInputs,
2169
2005
  ...dlcAccept.fundingInputs,
2170
- ];
2171
- allFundingInputs.sort((a, b) => Number(a.inputSerialId - b.inputSerialId));
2006
+ ]);
2172
2007
 
2173
2008
  // Create a map of input txid:vout to witness elements
2174
2009
  const witnessMap = new Map<string, ScriptWitnessV0[]>();
2175
2010
 
2176
- // Map witness elements correctly - CreateFundingSigsAlt only creates witness elements
2011
+ // Map witness elements correctly - CreateFundingSigs only creates witness elements
2177
2012
  // for the party's own inputs, so we need to map them correctly
2178
2013
 
2179
- // For dlcSign (offerer's signatures), map to offerer's inputs only
2014
+ // For dlcSign (offerer's signatures), map to offerer's inputs (including DLC inputs)
2180
2015
  let offererWitnessIndex = 0;
2181
2016
  dlcOffer.fundingInputs.forEach((fundingInput) => {
2182
- // Skip DLC inputs for now as in CreateFundingSigsAlt
2183
- if (fundingInput.dlcInput) {
2184
- return;
2185
- }
2186
-
2187
2017
  if (
2188
2018
  offererWitnessIndex < dlcSign.fundingSignatures.witnessElements.length
2189
2019
  ) {
@@ -2196,14 +2026,9 @@ export default class BitcoinDdkProvider extends Provider {
2196
2026
  }
2197
2027
  });
2198
2028
 
2199
- // For fundingSignatures (accepter's signatures), map to accepter's inputs only
2029
+ // For fundingSignatures (accepter's signatures), map to accepter's inputs (including DLC inputs)
2200
2030
  let accepterWitnessIndex = 0;
2201
2031
  dlcAccept.fundingInputs.forEach((fundingInput) => {
2202
- // Skip DLC inputs for now as in CreateFundingSigsAlt
2203
- if (fundingInput.dlcInput) {
2204
- return;
2205
- }
2206
-
2207
2032
  if (accepterWitnessIndex < fundingSignatures.witnessElements.length) {
2208
2033
  const key = `${fundingInput.prevTx.txId.toString()}:${fundingInput.prevTxVout}`;
2209
2034
  witnessMap.set(
@@ -2222,13 +2047,13 @@ export default class BitcoinDdkProvider extends Provider {
2222
2047
  for (const fundingInput of allFundingInputs) {
2223
2048
  const prevOut = fundingInput.prevTx.outputs[fundingInput.prevTxVout];
2224
2049
 
2225
- // Use same script handling as CreateFundingSigsAlt
2050
+ // Use same script handling as CreateFundingSigs
2226
2051
  const witnessUtxo = {
2227
2052
  script: Buffer.from(prevOut.scriptPubKey.serialize().subarray(1)),
2228
2053
  value: Number(prevOut.value.sats),
2229
2054
  };
2230
2055
 
2231
- // Use sequence from the original transaction (same as CreateFundingSigsAlt)
2056
+ // Use sequence from the original transaction (same as CreateFundingSigs)
2232
2057
  const originalInput = originalTransaction.ins.find(
2233
2058
  (input) =>
2234
2059
  input.hash.reverse().toString('hex') ===
@@ -2266,13 +2091,76 @@ export default class BitcoinDdkProvider extends Provider {
2266
2091
 
2267
2092
  const witnessElements = witnessMap.get(inputKey);
2268
2093
  if (witnessElements && witnessElements.length === 2) {
2269
- // Skip DLC inputs for now as requested
2094
+ // Handle DLC inputs with multisig finalization
2270
2095
  if (fundingInput.dlcInput) {
2096
+ const dlcInput = fundingInput.dlcInput;
2097
+
2098
+ // Create the multisig script for finalization
2099
+ // Use lexicographic ordering to match DDK library behavior
2100
+ const orderedPubkeys = orderPubkeysLexicographically(
2101
+ dlcInput.localFundPubkey,
2102
+ dlcInput.remoteFundPubkey,
2103
+ );
2104
+
2105
+ const paymentVariant = createP2WSHMultisigFromOrdered(
2106
+ orderedPubkeys,
2107
+ network,
2108
+ );
2109
+
2110
+ // Update the input with the witnessScript
2111
+ psbt.updateInput(inputIndex, {
2112
+ witnessScript: paymentVariant.redeem.output,
2113
+ });
2114
+
2115
+ // Sign this DLC input ourselves
2116
+ const dlcPrivKey = await this.findDlcFundingPrivateKey(
2117
+ dlcInput.localFundPubkey.toString('hex'),
2118
+ dlcInput.remoteFundPubkey.toString('hex'),
2119
+ );
2120
+ const keyPair = ECPair.fromPrivateKey(Buffer.from(dlcPrivKey, 'hex'));
2121
+
2122
+ psbt.signInput(inputIndex, keyPair);
2123
+
2124
+ // Get our signature and determine which pubkey it corresponds to
2125
+ const ourPartialSig = psbt.data.inputs[inputIndex].partialSig[0];
2126
+ const ourSig = ensureDerSignature(ourPartialSig.signature);
2127
+ const ourPubkey = ourPartialSig.pubkey;
2128
+
2129
+ // Get the other party's signature from witnessElements
2130
+ const otherPartySig = ensureDerSignature(witnessElements[0].witness);
2131
+
2132
+ // Order signatures according to the lexicographic pubkey order used in the multisig script
2133
+ let sig1: Buffer, sig2: Buffer;
2134
+
2135
+ if (Buffer.compare(ourPubkey, orderedPubkeys[0]) === 0) {
2136
+ // Our signature corresponds to the first pubkey in lexicographic order
2137
+ sig1 = ourSig;
2138
+ sig2 = otherPartySig;
2139
+ } else {
2140
+ // Our signature corresponds to the second pubkey in lexicographic order
2141
+ sig1 = otherPartySig;
2142
+ sig2 = ourSig;
2143
+ }
2144
+
2145
+ // Finalize with proper multisig witness: [OP_0, sig1, sig2, redeemScript]
2146
+ psbt.finalizeInput(inputIndex, () => ({
2147
+ finalScriptSig: Buffer.alloc(0),
2148
+ finalScriptWitness: Buffer.concat([
2149
+ Buffer.from([0x04]), // witness stack count
2150
+ Buffer.from([0x00]), // OP_0 (Bitcoin multisig bug)
2151
+ Buffer.from([sig1.length]),
2152
+ sig1,
2153
+ Buffer.from([sig2.length]),
2154
+ sig2,
2155
+ Buffer.from([paymentVariant.redeem.output.length]),
2156
+ paymentVariant.redeem.output,
2157
+ ]),
2158
+ }));
2271
2159
  continue;
2272
2160
  }
2273
2161
 
2274
2162
  // For P2WPKH inputs, finalize the input with witness data
2275
- const signature = this.ensureDerSignature(witnessElements[0].witness);
2163
+ const signature = ensureDerSignature(witnessElements[0].witness);
2276
2164
  const publicKey = witnessElements[1].witness;
2277
2165
 
2278
2166
  // Try a simpler approach - let bitcoinjs-lib handle witness construction
@@ -2376,15 +2264,13 @@ Payout Group not found',
2376
2264
  }
2377
2265
 
2378
2266
  async FindOutcomeIndexFromHyperbolaPayoutCurvePiece(
2379
- _dlcOffer: DlcOffer,
2267
+ dlcOffer: DlcOffer,
2380
2268
  contractDescriptor: NumericalDescriptor,
2381
2269
  contractOraclePairIndex: number,
2382
2270
  hyperbolaPayoutCurvePiece: HyperbolaPayoutCurvePiece,
2383
2271
  oracleAttestation: OracleAttestation,
2384
2272
  outcome: bigint,
2385
2273
  ): Promise<FindOutcomeResponse> {
2386
- const { dlcOffer } = checkTypes({ _dlcOffer });
2387
-
2388
2274
  const hyperbolaCurve = HyperbolaPayoutCurve.fromPayoutCurvePiece(
2389
2275
  hyperbolaPayoutCurvePiece,
2390
2276
  );
@@ -2914,21 +2800,15 @@ Payout Group not found even with brute force search',
2914
2800
 
2915
2801
  const network = await this.getConnectedNetwork();
2916
2802
 
2917
- const fundingPubKeys =
2918
- Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
2919
- ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
2920
- : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
2921
-
2922
- const p2ms = payments.p2ms({
2923
- m: 2,
2924
- pubkeys: fundingPubKeys,
2925
- network,
2926
- });
2803
+ const fundingPubKeys = orderPubkeysLexicographically(
2804
+ dlcOffer.fundingPubkey,
2805
+ dlcAccept.fundingPubkey,
2806
+ );
2927
2807
 
2928
- const paymentVariant = payments.p2wsh({
2929
- redeem: p2ms,
2808
+ const paymentVariant = createP2WSHMultisigFromOrdered(
2809
+ fundingPubKeys,
2930
2810
  network,
2931
- });
2811
+ );
2932
2812
 
2933
2813
  const sigHashRequestPromises: Promise<string>[] = [];
2934
2814
  const sigHashes = [];
@@ -3025,21 +2905,15 @@ Payout Group not found even with brute force search',
3025
2905
 
3026
2906
  const network = await this.getConnectedNetwork();
3027
2907
 
3028
- const fundingPubKeys =
3029
- Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
3030
- ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
3031
- : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
3032
-
3033
- const p2ms = payments.p2ms({
3034
- m: 2,
3035
- pubkeys: fundingPubKeys,
3036
- network,
3037
- });
2908
+ const fundingPubKeys = orderPubkeysLexicographically(
2909
+ dlcOffer.fundingPubkey,
2910
+ dlcAccept.fundingPubkey,
2911
+ );
3038
2912
 
3039
- const paymentVariant = payments.p2wsh({
3040
- redeem: p2ms,
2913
+ const paymentVariant = createP2WSHMultisigFromOrdered(
2914
+ fundingPubKeys,
3041
2915
  network,
3042
- });
2916
+ );
3043
2917
 
3044
2918
  const pubkey = isOfferer ? dlcAccept.fundingPubkey : dlcOffer.fundingPubkey;
3045
2919
 
@@ -3208,13 +3082,7 @@ Payout Group not found even with brute force search',
3208
3082
  ),
3209
3083
  );
3210
3084
 
3211
- const fundingInputs: FundingInput[] = _fundingInputs.map(
3212
- (input) => input as FundingInput,
3213
- );
3214
-
3215
- fundingInputs.sort(
3216
- (a, b) => Number(a.inputSerialId) - Number(b.inputSerialId),
3217
- );
3085
+ const fundingInputs = sortFundingInputsBySerialId(_fundingInputs);
3218
3086
 
3219
3087
  const fundOutputSerialId = generateSerialId();
3220
3088
 
@@ -3404,10 +3272,8 @@ Payout Group not found even with brute force search',
3404
3272
  ),
3405
3273
  );
3406
3274
 
3407
- fundingInputs = _fundingInputs.map((input) => input as FundingInput);
3408
-
3409
- fundingInputs.sort(
3410
- (a, b) => Number(a.inputSerialId) - Number(b.inputSerialId),
3275
+ fundingInputs = sortFundingInputsBySerialId(
3276
+ _fundingInputs.map((input) => input as FundingInput),
3411
3277
  );
3412
3278
  }
3413
3279
 
@@ -3492,7 +3358,7 @@ Payout Group not found even with brute force search',
3492
3358
 
3493
3359
  const _dlcTransactions = dlcTransactions;
3494
3360
 
3495
- const contractId = this.computeContractId(
3361
+ const contractId = computeContractId(
3496
3362
  _dlcTransactions.fundTx.txId.serialize(),
3497
3363
  _dlcTransactions.fundTxVout,
3498
3364
  dlcOffer.temporaryContractId,
@@ -3548,7 +3414,7 @@ Payout Group not found even with brute force search',
3548
3414
  true,
3549
3415
  );
3550
3416
 
3551
- const fundingSignatures = await this.CreateFundingSigsAlt(
3417
+ const fundingSignatures = await this.CreateFundingSigs(
3552
3418
  dlcOffer,
3553
3419
  dlcAccept,
3554
3420
  dlcTransactions,
@@ -3557,24 +3423,12 @@ Payout Group not found even with brute force search',
3557
3423
 
3558
3424
  const dlcTxs = dlcTransactions;
3559
3425
 
3560
- const contractId = this.computeContractId(
3426
+ const contractId = computeContractId(
3561
3427
  dlcTxs.fundTx.txId.serialize(),
3562
3428
  dlcTxs.fundTxVout,
3563
3429
  dlcOffer.temporaryContractId,
3564
3430
  );
3565
3431
 
3566
- assert(
3567
- Buffer.compare(
3568
- contractId,
3569
- this.computeContractId(
3570
- dlcTxs.fundTx.txId.serialize(),
3571
- dlcTxs.fundTxVout,
3572
- dlcOffer.temporaryContractId,
3573
- ),
3574
- ) === 0,
3575
- 'contractId must be the xor of funding txid, fundingOutputIndex and the tempContractId',
3576
- );
3577
-
3578
3432
  dlcTxs.contractId = contractId;
3579
3433
 
3580
3434
  dlcSign.contractId = contractId;
@@ -3601,6 +3455,17 @@ Payout Group not found even with brute force search',
3601
3455
  ): Promise<Tx> {
3602
3456
  let messagesList: Messages[] = [];
3603
3457
 
3458
+ const contractId = computeContractId(
3459
+ dlcTxs.fundTx.txId.serialize(),
3460
+ dlcTxs.fundTxVout,
3461
+ dlcOffer.temporaryContractId,
3462
+ );
3463
+
3464
+ assert(
3465
+ Buffer.compare(contractId, dlcSign.contractId) === 0,
3466
+ `Finalize Dlc Sign Contract ID mismatch: ${contractId.toString('hex')} !== ${dlcSign.contractId.toString('hex')}`,
3467
+ );
3468
+
3604
3469
  if (
3605
3470
  dlcOffer.contractInfo.type === MessageType.SingleContractInfo &&
3606
3471
  (dlcOffer.contractInfo as SingleContractInfo).contractDescriptor.type ===
@@ -3628,15 +3493,9 @@ Payout Group not found even with brute force search',
3628
3493
  false,
3629
3494
  );
3630
3495
 
3631
- await this.VerifyFundingSigsAlt(
3632
- dlcOffer,
3633
- dlcAccept,
3634
- dlcSign,
3635
- dlcTxs,
3636
- false,
3637
- );
3496
+ await this.VerifyFundingSigs(dlcOffer, dlcAccept, dlcSign, dlcTxs, false);
3638
3497
 
3639
- const fundingSignatures = await this.CreateFundingSigsAlt(
3498
+ const fundingSignatures = await this.CreateFundingSigs(
3640
3499
  dlcOffer,
3641
3500
  dlcAccept,
3642
3501
  dlcTxs,
@@ -3711,22 +3570,15 @@ Payout Group not found even with brute force search',
3711
3570
  );
3712
3571
  }
3713
3572
 
3714
- // Create the same funding script as createDlcClose
3715
- const fundingPubKeys =
3716
- Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
3717
- ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
3718
- : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
3719
-
3720
- const p2ms = payments.p2ms({
3721
- m: 2,
3722
- pubkeys: fundingPubKeys,
3723
- network,
3724
- });
3573
+ const fundingPubKeys = orderPubkeysLexicographically(
3574
+ dlcOffer.fundingPubkey,
3575
+ dlcAccept.fundingPubkey,
3576
+ );
3725
3577
 
3726
- const paymentVariant = payments.p2wsh({
3727
- redeem: p2ms,
3578
+ const paymentVariant = createP2WSHMultisigFromOrdered(
3579
+ fundingPubKeys,
3728
3580
  network,
3729
- });
3581
+ );
3730
3582
 
3731
3583
  // Add the funding input with sequence from refund transaction
3732
3584
  psbt.addInput({
@@ -3764,13 +3616,13 @@ Payout Group not found even with brute force search',
3764
3616
  // This is the offerer's pubkey, use dlcSign.refundSignature
3765
3617
  partialSigs.push({
3766
3618
  pubkey: pubkey,
3767
- signature: this.ensureDerSignature(dlcSign.refundSignature),
3619
+ signature: ensureDerSignature(dlcSign.refundSignature),
3768
3620
  });
3769
3621
  } else if (Buffer.compare(pubkey, dlcAccept.fundingPubkey) === 0) {
3770
3622
  // This is the accepter's pubkey, use dlcAccept.refundSignature
3771
3623
  partialSigs.push({
3772
3624
  pubkey: pubkey,
3773
- signature: this.ensureDerSignature(dlcAccept.refundSignature),
3625
+ signature: ensureDerSignature(dlcAccept.refundSignature),
3774
3626
  });
3775
3627
  }
3776
3628
  }
@@ -3843,21 +3695,11 @@ Payout Group not found even with brute force search',
3843
3695
  const network = await this.getConnectedNetwork();
3844
3696
  const psbt = new Psbt({ network });
3845
3697
 
3846
- const fundingPubKeys =
3847
- Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
3848
- ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
3849
- : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
3850
-
3851
- const p2ms = payments.p2ms({
3852
- m: 2,
3853
- pubkeys: fundingPubKeys,
3698
+ const paymentVariant = createP2WSHMultisig(
3699
+ dlcOffer.fundingPubkey,
3700
+ dlcAccept.fundingPubkey,
3854
3701
  network,
3855
- });
3856
-
3857
- const paymentVariant = payments.p2wsh({
3858
- redeem: p2ms,
3859
- network,
3860
- });
3702
+ );
3861
3703
 
3862
3704
  // Initiate and build PSBT
3863
3705
  let inputs: Input[] = _inputs;
@@ -4035,9 +3877,7 @@ Payout Group not found even with brute force search',
4035
3877
 
4036
3878
  // Extract close signature from psbt and decode it to only extract r and s values
4037
3879
  const closeSignature = await script.signature.decode(
4038
- this.ensureBuffer(
4039
- psbt.data.inputs[fundingInputIndex].partialSig[0].signature,
4040
- ),
3880
+ ensureBuffer(psbt.data.inputs[fundingInputIndex].partialSig[0].signature),
4041
3881
  ).signature;
4042
3882
 
4043
3883
  // Extract funding signatures from psbt
@@ -4049,9 +3889,9 @@ Payout Group not found even with brute force search',
4049
3889
  const witnessElements: ScriptWitnessV0[][] = [];
4050
3890
  for (let i = 0; i < inputSigs.length; i++) {
4051
3891
  const sigWitness = new ScriptWitnessV0();
4052
- sigWitness.witness = this.ensureBuffer(inputSigs[i].signature);
3892
+ sigWitness.witness = ensureBuffer(inputSigs[i].signature);
4053
3893
  const pubKeyWitness = new ScriptWitnessV0();
4054
- pubKeyWitness.witness = this.ensureBuffer(inputSigs[i].pubkey);
3894
+ pubKeyWitness.witness = ensureBuffer(inputSigs[i].pubkey);
4055
3895
  witnessElements.push([sigWitness, pubKeyWitness]);
4056
3896
  }
4057
3897
  const fundingSignatures = new FundingSignatures();
@@ -4297,21 +4137,11 @@ Payout Group not found even with brute force search',
4297
4137
  const network = await this.getConnectedNetwork();
4298
4138
  const psbt = new Psbt({ network });
4299
4139
 
4300
- const fundingPubKeys =
4301
- Buffer.compare(dlcOffer.fundingPubkey, dlcAccept.fundingPubkey) === -1
4302
- ? [dlcOffer.fundingPubkey, dlcAccept.fundingPubkey]
4303
- : [dlcAccept.fundingPubkey, dlcOffer.fundingPubkey];
4304
-
4305
- const p2ms = payments.p2ms({
4306
- m: 2,
4307
- pubkeys: fundingPubKeys,
4308
- network,
4309
- });
4310
-
4311
- const paymentVariant = payments.p2wsh({
4312
- redeem: p2ms,
4140
+ const paymentVariant = createP2WSHMultisig(
4141
+ dlcOffer.fundingPubkey,
4142
+ dlcAccept.fundingPubkey,
4313
4143
  network,
4314
- });
4144
+ );
4315
4145
 
4316
4146
  // Make temporary array to hold all inputs and then sort them
4317
4147
  // this method can be improved later
@@ -4645,16 +4475,10 @@ Payout Group not found even with brute force search',
4645
4475
  const network = await this.getConnectedNetwork();
4646
4476
 
4647
4477
  // Create 2-of-2 multisig payment using deterministic ordering
4648
- const p2ms = payments.p2ms({
4649
- m: 2,
4650
- pubkeys: orderedPubkeys,
4478
+ const paymentVariant = createP2WSHMultisigFromOrdered(
4479
+ orderedPubkeys,
4651
4480
  network,
4652
- });
4653
-
4654
- const paymentVariant = payments.p2wsh({
4655
- redeem: p2ms,
4656
- network,
4657
- });
4481
+ );
4658
4482
 
4659
4483
  const multisigAddress = paymentVariant.address!;
4660
4484