@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.
package/lib/index.ts CHANGED
@@ -1,2 +1,3 @@
1
1
  export { default } from './BitcoinDdkProvider';
2
2
  export * from './BitcoinDdkProvider';
3
+ export * from './utils/Utils';
@@ -5,8 +5,12 @@ import {
5
5
  DlcOffer,
6
6
  DlcSign,
7
7
  DlcTransactions,
8
+ FundingInput,
8
9
  MessageType,
9
10
  } from '@node-dlc/messaging';
11
+ import { BitcoinNetwork } from 'bitcoin-network';
12
+ import { payments } from 'bitcoinjs-lib';
13
+ import { Payment } from 'bitcoinjs-lib';
10
14
  import randomBytes from 'randombytes';
11
15
 
12
16
  export const asyncForEach = async (
@@ -116,3 +120,331 @@ interface OutputsToPayoutsResponse {
116
120
  payouts: PayoutRequest[];
117
121
  messagesList: Messages[];
118
122
  }
123
+
124
+ /**
125
+ * Orders public keys lexicographically for consistent multisig script creation.
126
+ * This ensures deterministic ordering regardless of which party creates the script.
127
+ *
128
+ * @param pubkey1 First public key buffer
129
+ * @param pubkey2 Second public key buffer
130
+ * @returns Array of public keys in lexicographic order [smaller, larger]
131
+ */
132
+ export function orderPubkeysLexicographically(
133
+ pubkey1: Buffer,
134
+ pubkey2: Buffer,
135
+ ): [Buffer, Buffer] {
136
+ return Buffer.compare(pubkey1, pubkey2) === -1
137
+ ? [pubkey1, pubkey2]
138
+ : [pubkey2, pubkey1];
139
+ }
140
+
141
+ /**
142
+ * Creates a 2-of-2 P2WSH multisig payment variant with lexicographically ordered pubkeys.
143
+ * This is the standard pattern used for DLC funding scripts.
144
+ *
145
+ * @param pubkey1 First public key buffer
146
+ * @param pubkey2 Second public key buffer
147
+ * @param network Bitcoin network
148
+ * @returns Payment variant with P2WSH multisig script
149
+ */
150
+ export function createP2WSHMultisig(
151
+ pubkey1: Buffer,
152
+ pubkey2: Buffer,
153
+ network: BitcoinNetwork,
154
+ ): Payment {
155
+ const orderedPubkeys = orderPubkeysLexicographically(pubkey1, pubkey2);
156
+
157
+ const p2ms = payments.p2ms({
158
+ m: 2,
159
+ pubkeys: orderedPubkeys,
160
+ network,
161
+ });
162
+
163
+ return payments.p2wsh({
164
+ redeem: p2ms,
165
+ network,
166
+ });
167
+ }
168
+
169
+ export function createP2MSMultisig(
170
+ pubkey1: Buffer,
171
+ pubkey2: Buffer,
172
+ network: BitcoinNetwork,
173
+ ): Payment {
174
+ const orderedPubkeys = orderPubkeysLexicographically(pubkey1, pubkey2);
175
+
176
+ return payments.p2ms({
177
+ m: 2,
178
+ pubkeys: orderedPubkeys,
179
+ network,
180
+ });
181
+ }
182
+
183
+ /**
184
+ * Creates a 2-of-2 P2WSH multisig payment variant from pre-ordered pubkeys.
185
+ * Use this when you already have the pubkeys in the correct order.
186
+ *
187
+ * @param orderedPubkeys Array of public key buffers in the desired order
188
+ * @param network Bitcoin network
189
+ * @returns Payment variant with P2WSH multisig script
190
+ */
191
+ export function createP2WSHMultisigFromOrdered(
192
+ orderedPubkeys: Buffer[],
193
+ network: BitcoinNetwork,
194
+ ): Payment {
195
+ const p2ms = payments.p2ms({
196
+ m: 2,
197
+ pubkeys: orderedPubkeys,
198
+ network,
199
+ });
200
+
201
+ return payments.p2wsh({
202
+ redeem: p2ms,
203
+ network,
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Creates a 2-of-2 multisig script (P2MS) from lexicographically ordered pubkeys.
209
+ * Returns just the multisig script, not wrapped in P2WSH.
210
+ *
211
+ * @param pubkey1 First public key buffer
212
+ * @param pubkey2 Second public key buffer
213
+ * @param network Bitcoin network
214
+ * @returns Multisig payment script
215
+ */
216
+ export function createMultisigScript(
217
+ pubkey1: Buffer,
218
+ pubkey2: Buffer,
219
+ network: BitcoinNetwork,
220
+ ): Payment {
221
+ const orderedPubkeys = orderPubkeysLexicographically(pubkey1, pubkey2);
222
+
223
+ return payments.p2ms({
224
+ m: 2,
225
+ pubkeys: orderedPubkeys,
226
+ network,
227
+ });
228
+ }
229
+
230
+ /**
231
+ * Helper function to ensure we have a Buffer object
232
+ * Handles cases where Buffer objects have been serialized/deserialized
233
+ */
234
+ export function ensureBuffer(
235
+ bufferLike: Buffer | { type: string; data: number[] } | any,
236
+ ): Buffer {
237
+ if (Buffer.isBuffer(bufferLike)) {
238
+ return bufferLike;
239
+ }
240
+ if (bufferLike && bufferLike.type === 'Buffer' && bufferLike.data) {
241
+ return Buffer.from(bufferLike.data);
242
+ }
243
+ return bufferLike;
244
+ }
245
+
246
+ /**
247
+ * Detect if signature is in compact format (64 bytes) or DER format
248
+ * and convert compact to DER if needed, adding SIGHASH_ALL flag
249
+ */
250
+ export function ensureDerSignature(signature: Buffer): Buffer {
251
+ // If signature is 64 bytes, it's likely compact format (32-byte r + 32-byte s)
252
+ if (signature.length === 64) {
253
+ // Convert compact signature to DER format
254
+ const r = signature.subarray(0, 32);
255
+ const s = signature.subarray(32, 64);
256
+
257
+ // Create DER encoding manually
258
+ // DER format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S]
259
+
260
+ // Remove leading zeros from r and s, but keep at least one byte
261
+ let rBytes = r;
262
+ while (
263
+ rBytes.length > 1 &&
264
+ rBytes[0] === 0x00 &&
265
+ (rBytes[1] & 0x80) === 0
266
+ ) {
267
+ rBytes = rBytes.subarray(1);
268
+ }
269
+
270
+ let sBytes = s;
271
+ while (
272
+ sBytes.length > 1 &&
273
+ sBytes[0] === 0x00 &&
274
+ (sBytes[1] & 0x80) === 0
275
+ ) {
276
+ sBytes = sBytes.subarray(1);
277
+ }
278
+
279
+ // Add padding byte if high bit is set (to keep numbers positive)
280
+ if ((rBytes[0] & 0x80) !== 0) {
281
+ rBytes = Buffer.concat([Buffer.from([0x00]), rBytes]);
282
+ }
283
+ if ((sBytes[0] & 0x80) !== 0) {
284
+ sBytes = Buffer.concat([Buffer.from([0x00]), sBytes]);
285
+ }
286
+
287
+ const totalLength = 2 + rBytes.length + 2 + sBytes.length;
288
+
289
+ const derSignature = Buffer.concat([
290
+ Buffer.from([0x30, totalLength]), // SEQUENCE tag and total length
291
+ Buffer.from([0x02, rBytes.length]), // INTEGER tag and R length
292
+ rBytes,
293
+ Buffer.from([0x02, sBytes.length]), // INTEGER tag and S length
294
+ sBytes,
295
+ Buffer.from([0x01]), // SIGHASH_ALL flag
296
+ ]);
297
+
298
+ return derSignature;
299
+ }
300
+
301
+ // If it's already DER format, check if it has SIGHASH flag
302
+ if (signature.length > 0 && signature[0] === 0x30) {
303
+ // Check if it already has a SIGHASH flag (last byte should be 0x01 for SIGHASH_ALL)
304
+ if (signature[signature.length - 1] !== 0x01) {
305
+ // Add SIGHASH_ALL flag
306
+ return Buffer.concat([signature, Buffer.from([0x01])]);
307
+ }
308
+ return signature;
309
+ }
310
+
311
+ // For other formats, return as-is
312
+ return signature;
313
+ }
314
+
315
+ /**
316
+ * Detect if signature is in DER format and convert to compact format (64 bytes)
317
+ * by extracting r and s values, removing SIGHASH flag if present
318
+ */
319
+ export function ensureCompactSignature(signature: Buffer): Buffer {
320
+ // If signature is already 64 bytes, it's likely already compact format
321
+ if (signature.length === 64) {
322
+ return signature;
323
+ }
324
+
325
+ // Check if it's DER format (starts with 0x30)
326
+ if (signature.length > 6 && signature[0] === 0x30) {
327
+ let derSig = signature;
328
+
329
+ // Remove SIGHASH flag if present (last byte is typically 0x01 for SIGHASH_ALL)
330
+ if (signature[signature.length - 1] === 0x01) {
331
+ derSig = signature.subarray(0, -1);
332
+ }
333
+
334
+ // Parse DER format: 0x30 [total-length] 0x02 [R-length] [R] 0x02 [S-length] [S]
335
+ if (derSig[0] !== 0x30) {
336
+ throw new Error('Invalid DER signature: missing SEQUENCE tag');
337
+ }
338
+
339
+ const totalLength = derSig[1];
340
+ if (derSig.length < totalLength + 2) {
341
+ throw new Error('Invalid DER signature: length mismatch');
342
+ }
343
+
344
+ let offset = 2;
345
+
346
+ // Parse R value
347
+ if (derSig[offset] !== 0x02) {
348
+ throw new Error('Invalid DER signature: missing INTEGER tag for R');
349
+ }
350
+ offset++;
351
+
352
+ const rLength = derSig[offset];
353
+ offset++;
354
+
355
+ if (offset + rLength > derSig.length) {
356
+ throw new Error(
357
+ 'Invalid DER signature: R length exceeds signature length',
358
+ );
359
+ }
360
+
361
+ let rBytes = derSig.subarray(offset, offset + rLength);
362
+ offset += rLength;
363
+
364
+ // Parse S value
365
+ if (derSig[offset] !== 0x02) {
366
+ throw new Error('Invalid DER signature: missing INTEGER tag for S');
367
+ }
368
+ offset++;
369
+
370
+ const sLength = derSig[offset];
371
+ offset++;
372
+
373
+ if (offset + sLength > derSig.length) {
374
+ throw new Error(
375
+ 'Invalid DER signature: S length exceeds signature length',
376
+ );
377
+ }
378
+
379
+ let sBytes = derSig.subarray(offset, offset + sLength);
380
+
381
+ // Remove leading zero padding from r and s (DER may pad to prevent negative interpretation)
382
+ while (rBytes.length > 1 && rBytes[0] === 0x00) {
383
+ rBytes = rBytes.subarray(1);
384
+ }
385
+ while (sBytes.length > 1 && sBytes[0] === 0x00) {
386
+ sBytes = sBytes.subarray(1);
387
+ }
388
+
389
+ // Pad to 32 bytes each (compact format requires exactly 32 bytes for r and s)
390
+ while (rBytes.length < 32) {
391
+ rBytes = Buffer.concat([Buffer.from([0x00]), rBytes]);
392
+ }
393
+ while (sBytes.length < 32) {
394
+ sBytes = Buffer.concat([Buffer.from([0x00]), sBytes]);
395
+ }
396
+
397
+ if (rBytes.length !== 32 || sBytes.length !== 32) {
398
+ throw new Error('Invalid signature values: r or s exceeds 32 bytes');
399
+ }
400
+
401
+ // Combine r and s into 64-byte compact format
402
+ return Buffer.concat([rBytes, sBytes]);
403
+ }
404
+
405
+ // For other formats, throw error as we can't convert
406
+ throw new Error(
407
+ 'Unable to convert signature to compact format: unknown format',
408
+ );
409
+ }
410
+
411
+ /**
412
+ * Compute contract ID from fund transaction ID, output index, and temporary contract ID
413
+ * Matches the Rust implementation in rust-dlc
414
+ */
415
+ export function computeContractId(
416
+ fundTxId: Buffer,
417
+ fundOutputIndex: number,
418
+ temporaryContractId: Buffer,
419
+ ): Buffer {
420
+ if (fundTxId.length !== 32) {
421
+ throw new Error('Fund transaction ID must be 32 bytes');
422
+ }
423
+ if (temporaryContractId.length !== 32) {
424
+ throw new Error('Temporary contract ID must be 32 bytes');
425
+ }
426
+ if (fundOutputIndex > 0xffff) {
427
+ throw new Error('Fund output index must fit in 16 bits');
428
+ }
429
+
430
+ const result = Buffer.alloc(32);
431
+
432
+ // XOR fund_tx_id with temporary_id, with byte order reversal for fund_tx_id
433
+ for (let i = 0; i < 32; i++) {
434
+ result[i] = fundTxId[31 - i] ^ temporaryContractId[i];
435
+ }
436
+
437
+ // XOR the fund output index into the last two bytes
438
+ result[30] ^= (fundOutputIndex >> 8) & 0xff; // High byte
439
+ result[31] ^= fundOutputIndex & 0xff; // Low byte
440
+
441
+ return result;
442
+ }
443
+
444
+ export function sortFundingInputsBySerialId(
445
+ inputs: FundingInput[],
446
+ ): FundingInput[] {
447
+ return inputs.sort(
448
+ (a, b) => Number(a.inputSerialId) - Number(b.inputSerialId),
449
+ );
450
+ }
package/package.json CHANGED
@@ -1,17 +1,17 @@
1
1
  {
2
2
  "name": "@atomicfinance/bitcoin-ddk-provider",
3
3
  "umdName": "BitcoinDdkProvider",
4
- "version": "4.1.13",
4
+ "version": "4.2.0",
5
5
  "description": "Bitcoin Abstraction Layer Ddk Provider",
6
6
  "author": "Atomic Finance <info@atomic.finance>",
7
7
  "homepage": "",
8
8
  "license": "ISC",
9
9
  "main": "dist/index.js",
10
10
  "dependencies": {
11
- "@atomicfinance/bitcoin-utils": "4.1.13",
12
- "@atomicfinance/provider": "^4.1.13",
13
- "@atomicfinance/types": "^4.1.13",
14
- "@atomicfinance/utils": "^4.1.13",
11
+ "@atomicfinance/bitcoin-utils": "4.2.0",
12
+ "@atomicfinance/provider": "^4.2.0",
13
+ "@atomicfinance/types": "^4.2.0",
14
+ "@atomicfinance/utils": "^4.2.0",
15
15
  "bignumber.js": "^9.0.1",
16
16
  "bip66": "^1.1.5",
17
17
  "bitcoin-network": "0.1.0",