@btc-vision/bitcoin 7.0.0-alpha.2 → 7.0.0-alpha.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/README.md +455 -155
  2. package/browser/chunks/WorkerSigningPool.sequential-DHha7j0b.js +113 -0
  3. package/browser/ecc/context.d.ts +22 -21
  4. package/browser/ecc/context.d.ts.map +1 -1
  5. package/browser/ecc/index.d.ts +1 -1
  6. package/browser/ecc/index.d.ts.map +1 -1
  7. package/browser/ecc/types.d.ts +10 -123
  8. package/browser/ecc/types.d.ts.map +1 -1
  9. package/browser/env.d.ts +13 -0
  10. package/browser/env.d.ts.map +1 -0
  11. package/browser/index.d.ts +3 -3
  12. package/browser/index.d.ts.map +1 -1
  13. package/browser/index.js +5790 -4295
  14. package/browser/io/index.d.ts +0 -1
  15. package/browser/io/index.d.ts.map +1 -1
  16. package/browser/payments/p2tr.d.ts.map +1 -1
  17. package/browser/psbt/types.d.ts +2 -68
  18. package/browser/psbt/types.d.ts.map +1 -1
  19. package/browser/psbt.d.ts +9 -11
  20. package/browser/psbt.d.ts.map +1 -1
  21. package/browser/types.d.ts +1 -1
  22. package/browser/types.d.ts.map +1 -1
  23. package/browser/workers/WorkerSigningPool.d.ts +6 -0
  24. package/browser/workers/WorkerSigningPool.d.ts.map +1 -1
  25. package/browser/workers/WorkerSigningPool.node.d.ts +6 -0
  26. package/browser/workers/WorkerSigningPool.node.d.ts.map +1 -1
  27. package/browser/workers/WorkerSigningPool.sequential.d.ts +69 -0
  28. package/browser/workers/WorkerSigningPool.sequential.d.ts.map +1 -0
  29. package/browser/workers/WorkerSigningPool.worklet.d.ts +64 -0
  30. package/browser/workers/WorkerSigningPool.worklet.d.ts.map +1 -0
  31. package/browser/workers/index.d.ts +2 -2
  32. package/browser/workers/index.d.ts.map +1 -1
  33. package/browser/workers/index.react-native.d.ts +28 -0
  34. package/browser/workers/index.react-native.d.ts.map +1 -0
  35. package/browser/workers/psbt-parallel.d.ts +2 -3
  36. package/browser/workers/psbt-parallel.d.ts.map +1 -1
  37. package/browser/workers/types.d.ts +12 -0
  38. package/browser/workers/types.d.ts.map +1 -1
  39. package/build/ecc/context.d.ts +22 -21
  40. package/build/ecc/context.d.ts.map +1 -1
  41. package/build/ecc/context.js +19 -114
  42. package/build/ecc/context.js.map +1 -1
  43. package/build/ecc/index.d.ts +1 -1
  44. package/build/ecc/index.d.ts.map +1 -1
  45. package/build/ecc/types.d.ts +7 -126
  46. package/build/ecc/types.d.ts.map +1 -1
  47. package/build/ecc/types.js +4 -1
  48. package/build/ecc/types.js.map +1 -1
  49. package/build/env.d.ts +13 -0
  50. package/build/env.d.ts.map +1 -0
  51. package/build/env.js +198 -0
  52. package/build/env.js.map +1 -0
  53. package/build/index.d.ts +4 -3
  54. package/build/index.d.ts.map +1 -1
  55. package/build/index.js +2 -1
  56. package/build/index.js.map +1 -1
  57. package/build/io/index.d.ts +0 -1
  58. package/build/io/index.d.ts.map +1 -1
  59. package/build/io/index.js +0 -2
  60. package/build/io/index.js.map +1 -1
  61. package/build/payments/p2tr.d.ts.map +1 -1
  62. package/build/payments/p2tr.js +2 -3
  63. package/build/payments/p2tr.js.map +1 -1
  64. package/build/psbt/types.d.ts +2 -68
  65. package/build/psbt/types.d.ts.map +1 -1
  66. package/build/psbt.d.ts +9 -11
  67. package/build/psbt.d.ts.map +1 -1
  68. package/build/psbt.js +38 -53
  69. package/build/psbt.js.map +1 -1
  70. package/build/tsconfig.build.tsbuildinfo +1 -1
  71. package/build/types.d.ts +1 -1
  72. package/build/types.d.ts.map +1 -1
  73. package/build/types.js +2 -16
  74. package/build/types.js.map +1 -1
  75. package/build/workers/WorkerSigningPool.d.ts +6 -0
  76. package/build/workers/WorkerSigningPool.d.ts.map +1 -1
  77. package/build/workers/WorkerSigningPool.js +8 -0
  78. package/build/workers/WorkerSigningPool.js.map +1 -1
  79. package/build/workers/WorkerSigningPool.node.d.ts +6 -0
  80. package/build/workers/WorkerSigningPool.node.d.ts.map +1 -1
  81. package/build/workers/WorkerSigningPool.node.js +9 -2
  82. package/build/workers/WorkerSigningPool.node.js.map +1 -1
  83. package/build/workers/WorkerSigningPool.sequential.d.ts +78 -0
  84. package/build/workers/WorkerSigningPool.sequential.d.ts.map +1 -0
  85. package/build/workers/WorkerSigningPool.sequential.js +160 -0
  86. package/build/workers/WorkerSigningPool.sequential.js.map +1 -0
  87. package/build/workers/WorkerSigningPool.worklet.d.ts +79 -0
  88. package/build/workers/WorkerSigningPool.worklet.d.ts.map +1 -0
  89. package/build/workers/WorkerSigningPool.worklet.js +388 -0
  90. package/build/workers/WorkerSigningPool.worklet.js.map +1 -0
  91. package/build/workers/index.d.ts +2 -2
  92. package/build/workers/index.d.ts.map +1 -1
  93. package/build/workers/index.js +9 -0
  94. package/build/workers/index.js.map +1 -1
  95. package/build/workers/index.react-native.d.ts +28 -0
  96. package/build/workers/index.react-native.d.ts.map +1 -0
  97. package/build/workers/index.react-native.js +67 -0
  98. package/build/workers/index.react-native.js.map +1 -0
  99. package/build/workers/psbt-parallel.d.ts +2 -3
  100. package/build/workers/psbt-parallel.d.ts.map +1 -1
  101. package/build/workers/psbt-parallel.js +4 -4
  102. package/build/workers/psbt-parallel.js.map +1 -1
  103. package/build/workers/types.d.ts +12 -0
  104. package/build/workers/types.d.ts.map +1 -1
  105. package/package.json +14 -4
  106. package/src/ecc/context.ts +26 -147
  107. package/src/ecc/index.ts +2 -2
  108. package/src/ecc/types.ts +7 -138
  109. package/src/env.ts +237 -0
  110. package/src/index.ts +2 -4
  111. package/src/io/index.ts +0 -3
  112. package/src/payments/p2tr.ts +2 -2
  113. package/src/psbt/types.ts +2 -84
  114. package/src/psbt.ts +63 -121
  115. package/src/types.ts +5 -28
  116. package/src/workers/WorkerSigningPool.node.ts +10 -2
  117. package/src/workers/WorkerSigningPool.sequential.ts +190 -0
  118. package/src/workers/WorkerSigningPool.ts +9 -0
  119. package/src/workers/WorkerSigningPool.worklet.ts +519 -0
  120. package/src/workers/index.react-native.ts +110 -0
  121. package/src/workers/index.ts +10 -1
  122. package/src/workers/psbt-parallel.ts +8 -8
  123. package/src/workers/types.ts +16 -0
  124. package/test/env.spec.ts +418 -0
  125. package/test/workers-pool.spec.ts +43 -0
  126. package/test/workers-sequential.spec.ts +669 -0
  127. package/test/workers-worklet.spec.ts +500 -0
  128. package/browser/io/MemoryPool.d.ts +0 -220
  129. package/browser/io/MemoryPool.d.ts.map +0 -1
  130. package/build/io/MemoryPool.d.ts +0 -220
  131. package/build/io/MemoryPool.d.ts.map +0 -1
  132. package/build/io/MemoryPool.js +0 -309
  133. package/build/io/MemoryPool.js.map +0 -1
  134. package/src/io/MemoryPool.ts +0 -343
@@ -0,0 +1,669 @@
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
2
+ import {
3
+ type ParallelSignerKeyPair,
4
+ SignatureType,
5
+ type SigningTask,
6
+ } from '../src/workers/types.js';
7
+ import { SequentialSigningPool } from '../src/workers/WorkerSigningPool.sequential.js';
8
+ import { EccContext } from '../src/ecc/context.js';
9
+ import {
10
+ createNobleBackend,
11
+ createPrivateKey,
12
+ createMessageHash,
13
+ createPublicKey,
14
+ createXOnlyPublicKey,
15
+ createSignature,
16
+ createSchnorrSignature,
17
+ } from '@btc-vision/ecpair';
18
+
19
+ const nobleBackend = createNobleBackend();
20
+
21
+ /**
22
+ * Returns a valid secp256k1 private key (scalar = 1).
23
+ * Every call returns a fresh copy so tests can verify zeroing.
24
+ */
25
+ function makeTestPrivateKey(): Uint8Array {
26
+ const key = new Uint8Array(32);
27
+ key[31] = 0x01;
28
+ return key;
29
+ }
30
+
31
+ /** Derives the compressed public key for the test private key. */
32
+ function getTestPublicKey(): Uint8Array {
33
+ return nobleBackend.pointFromScalar(createPrivateKey(makeTestPrivateKey()))!;
34
+ }
35
+
36
+ describe('SequentialSigningPool', () => {
37
+ beforeEach(() => {
38
+ SequentialSigningPool.resetInstance();
39
+ EccContext.init(nobleBackend);
40
+ });
41
+
42
+ afterEach(() => {
43
+ SequentialSigningPool.resetInstance();
44
+ EccContext.clear();
45
+ });
46
+
47
+ describe('Singleton Pattern', () => {
48
+ it('should return same instance on multiple calls', () => {
49
+ const pool1 = SequentialSigningPool.getInstance();
50
+ const pool2 = SequentialSigningPool.getInstance();
51
+ expect(pool1).toBe(pool2);
52
+ });
53
+
54
+ it('should create new instance after reset', () => {
55
+ const pool1 = SequentialSigningPool.getInstance();
56
+ SequentialSigningPool.resetInstance();
57
+ const pool2 = SequentialSigningPool.getInstance();
58
+ expect(pool1).not.toBe(pool2);
59
+ });
60
+
61
+ it('should accept config on first call', () => {
62
+ const pool = SequentialSigningPool.getInstance({ taskTimeoutMs: 5000 });
63
+ expect(pool).toBeDefined();
64
+ });
65
+ });
66
+
67
+ describe('Pool Properties', () => {
68
+ it('should report workerCount as 0', () => {
69
+ const pool = SequentialSigningPool.getInstance();
70
+ expect(pool.workerCount).toBe(0);
71
+ });
72
+
73
+ it('should report idleWorkerCount as 0', () => {
74
+ const pool = SequentialSigningPool.getInstance();
75
+ expect(pool.idleWorkerCount).toBe(0);
76
+ });
77
+
78
+ it('should report busyWorkerCount as 0', () => {
79
+ const pool = SequentialSigningPool.getInstance();
80
+ expect(pool.busyWorkerCount).toBe(0);
81
+ });
82
+
83
+ it('should report isPreservingWorkers as false', () => {
84
+ const pool = SequentialSigningPool.getInstance();
85
+ expect(pool.isPreservingWorkers).toBe(false);
86
+ });
87
+ });
88
+
89
+ describe('No-op Methods', () => {
90
+ it('should initialize without error', async () => {
91
+ const pool = SequentialSigningPool.getInstance();
92
+ await expect(pool.initialize()).resolves.toBeUndefined();
93
+ });
94
+
95
+ it('should shutdown without error', async () => {
96
+ const pool = SequentialSigningPool.getInstance();
97
+ await expect(pool.shutdown()).resolves.toBeUndefined();
98
+ });
99
+
100
+ it('should preserve/release without error', () => {
101
+ const pool = SequentialSigningPool.getInstance();
102
+ pool.preserveWorkers();
103
+ pool.releaseWorkers();
104
+ // No-ops — should not throw
105
+ });
106
+ });
107
+
108
+ describe('signBatch', () => {
109
+ it('should return success for empty task array', async () => {
110
+ const pool = SequentialSigningPool.getInstance();
111
+
112
+ const keyPair: ParallelSignerKeyPair = {
113
+ publicKey: getTestPublicKey(),
114
+ getPrivateKey: makeTestPrivateKey,
115
+ };
116
+
117
+ const result = await pool.signBatch([], keyPair);
118
+
119
+ expect(result.success).toBe(true);
120
+ expect(result.signatures.size).toBe(0);
121
+ expect(result.errors.size).toBe(0);
122
+ expect(result.durationMs).toBeGreaterThanOrEqual(0);
123
+ });
124
+
125
+ it('should produce valid ECDSA signatures', async () => {
126
+ const pool = SequentialSigningPool.getInstance();
127
+ const hash = new Uint8Array(32).fill(0x11);
128
+ const pubKey = getTestPublicKey();
129
+
130
+ const keyPair: ParallelSignerKeyPair = {
131
+ publicKey: pubKey,
132
+ getPrivateKey: makeTestPrivateKey,
133
+ };
134
+
135
+ const tasks: SigningTask[] = [
136
+ {
137
+ taskId: 'ecdsa-0',
138
+ inputIndex: 0,
139
+ hash,
140
+ signatureType: SignatureType.ECDSA,
141
+ sighashType: 0x01,
142
+ },
143
+ ];
144
+
145
+ const result = await pool.signBatch(tasks, keyPair);
146
+
147
+ expect(result.success).toBe(true);
148
+ expect(result.signatures.size).toBe(1);
149
+ expect(result.errors.size).toBe(0);
150
+
151
+ const sig = result.signatures.get(0)!;
152
+ expect(sig.type).toBe('result');
153
+ expect(sig.taskId).toBe('ecdsa-0');
154
+ expect(sig.inputIndex).toBe(0);
155
+ expect(sig.signatureType).toBe(SignatureType.ECDSA);
156
+ expect(sig.signature).toHaveLength(64);
157
+
158
+ // Verify signature with properly typed branded values
159
+ const valid = nobleBackend.verify(
160
+ createMessageHash(hash),
161
+ createPublicKey(pubKey),
162
+ createSignature(sig.signature),
163
+ );
164
+ expect(valid).toBe(true);
165
+ });
166
+
167
+ it('should produce valid Schnorr signatures', async () => {
168
+ const pool = SequentialSigningPool.getInstance();
169
+ const hash = new Uint8Array(32).fill(0x22);
170
+ const pubKey = getTestPublicKey();
171
+
172
+ const keyPair: ParallelSignerKeyPair = {
173
+ publicKey: pubKey,
174
+ getPrivateKey: makeTestPrivateKey,
175
+ };
176
+
177
+ const tasks: SigningTask[] = [
178
+ {
179
+ taskId: 'schnorr-0',
180
+ inputIndex: 0,
181
+ hash,
182
+ signatureType: SignatureType.Schnorr,
183
+ sighashType: 0x00,
184
+ },
185
+ ];
186
+
187
+ const result = await pool.signBatch(tasks, keyPair);
188
+
189
+ expect(result.success).toBe(true);
190
+ expect(result.signatures.size).toBe(1);
191
+
192
+ const sig = result.signatures.get(0)!;
193
+ expect(sig.signatureType).toBe(SignatureType.Schnorr);
194
+ expect(sig.signature).toHaveLength(64);
195
+
196
+ // Verify Schnorr signature with x-only pubkey (32 bytes, strip prefix)
197
+ const valid = nobleBackend.verifySchnorr!(
198
+ createMessageHash(hash),
199
+ createXOnlyPublicKey(pubKey.subarray(1, 33)),
200
+ createSchnorrSignature(sig.signature),
201
+ );
202
+ expect(valid).toBe(true);
203
+ });
204
+
205
+ it('should handle mixed ECDSA and Schnorr tasks', async () => {
206
+ const pool = SequentialSigningPool.getInstance();
207
+
208
+ const keyPair: ParallelSignerKeyPair = {
209
+ publicKey: getTestPublicKey(),
210
+ getPrivateKey: makeTestPrivateKey,
211
+ };
212
+
213
+ const tasks: SigningTask[] = [
214
+ {
215
+ taskId: 'ecdsa-0',
216
+ inputIndex: 0,
217
+ hash: new Uint8Array(32).fill(0x11),
218
+ signatureType: SignatureType.ECDSA,
219
+ sighashType: 0x01,
220
+ },
221
+ {
222
+ taskId: 'schnorr-1',
223
+ inputIndex: 1,
224
+ hash: new Uint8Array(32).fill(0x22),
225
+ signatureType: SignatureType.Schnorr,
226
+ sighashType: 0x00,
227
+ },
228
+ ];
229
+
230
+ const result = await pool.signBatch(tasks, keyPair);
231
+
232
+ expect(result.success).toBe(true);
233
+ expect(result.signatures.size).toBe(2);
234
+ expect(result.signatures.get(0)?.signatureType).toBe(SignatureType.ECDSA);
235
+ expect(result.signatures.get(1)?.signatureType).toBe(SignatureType.Schnorr);
236
+ });
237
+
238
+ it('should include leafHash for Taproot script-path', async () => {
239
+ const pool = SequentialSigningPool.getInstance();
240
+ const leafHash = new Uint8Array(32).fill(0xcd);
241
+
242
+ const keyPair: ParallelSignerKeyPair = {
243
+ publicKey: getTestPublicKey(),
244
+ getPrivateKey: makeTestPrivateKey,
245
+ };
246
+
247
+ const tasks: SigningTask[] = [
248
+ {
249
+ taskId: 'script-path-0',
250
+ inputIndex: 0,
251
+ hash: new Uint8Array(32).fill(0x33),
252
+ signatureType: SignatureType.Schnorr,
253
+ sighashType: 0x00,
254
+ leafHash,
255
+ },
256
+ ];
257
+
258
+ const result = await pool.signBatch(tasks, keyPair);
259
+
260
+ expect(result.success).toBe(true);
261
+ expect(result.signatures.get(0)?.leafHash).toBeDefined();
262
+ expect(result.signatures.get(0)?.leafHash).toEqual(leafHash);
263
+ });
264
+
265
+ it('should report duration', async () => {
266
+ const pool = SequentialSigningPool.getInstance();
267
+
268
+ const keyPair: ParallelSignerKeyPair = {
269
+ publicKey: getTestPublicKey(),
270
+ getPrivateKey: makeTestPrivateKey,
271
+ };
272
+
273
+ const tasks: SigningTask[] = [
274
+ {
275
+ taskId: 'task-0',
276
+ inputIndex: 0,
277
+ hash: new Uint8Array(32).fill(0x11),
278
+ signatureType: SignatureType.ECDSA,
279
+ sighashType: 0x01,
280
+ },
281
+ ];
282
+
283
+ const result = await pool.signBatch(tasks, keyPair);
284
+ expect(result.durationMs).toBeGreaterThanOrEqual(0);
285
+ });
286
+
287
+ it('should handle multiple inputs sequentially', async () => {
288
+ const pool = SequentialSigningPool.getInstance();
289
+
290
+ const keyPair: ParallelSignerKeyPair = {
291
+ publicKey: getTestPublicKey(),
292
+ getPrivateKey: makeTestPrivateKey,
293
+ };
294
+
295
+ const tasks: SigningTask[] = [];
296
+ for (let i = 0; i < 20; i++) {
297
+ tasks.push({
298
+ taskId: `task-${i}`,
299
+ inputIndex: i,
300
+ hash: new Uint8Array(32).fill(i),
301
+ signatureType: i % 2 === 0 ? SignatureType.ECDSA : SignatureType.Schnorr,
302
+ sighashType: i % 2 === 0 ? 0x01 : 0x00,
303
+ });
304
+ }
305
+
306
+ const result = await pool.signBatch(tasks, keyPair);
307
+
308
+ expect(result.success).toBe(true);
309
+ expect(result.signatures.size).toBe(20);
310
+ expect(result.errors.size).toBe(0);
311
+
312
+ for (let i = 0; i < 20; i++) {
313
+ expect(result.signatures.has(i)).toBe(true);
314
+ }
315
+ });
316
+ });
317
+
318
+ describe('Error Handling', () => {
319
+ it('should report error for invalid private key (zero)', async () => {
320
+ const pool = SequentialSigningPool.getInstance();
321
+
322
+ const keyPair: ParallelSignerKeyPair = {
323
+ publicKey: new Uint8Array(33).fill(0x02),
324
+ // Zero key is invalid for secp256k1
325
+ getPrivateKey: () => new Uint8Array(32),
326
+ };
327
+
328
+ const tasks: SigningTask[] = [
329
+ {
330
+ taskId: 'bad-0',
331
+ inputIndex: 0,
332
+ hash: new Uint8Array(32).fill(0x11),
333
+ signatureType: SignatureType.ECDSA,
334
+ sighashType: 0x01,
335
+ },
336
+ ];
337
+
338
+ const result = await pool.signBatch(tasks, keyPair);
339
+
340
+ expect(result.success).toBe(false);
341
+ expect(result.errors.size).toBe(1);
342
+ expect(result.errors.has(0)).toBe(true);
343
+ });
344
+
345
+ it('should report error for bad Schnorr signing operation', async () => {
346
+ const pool = SequentialSigningPool.getInstance();
347
+
348
+ // Spy on signSchnorr to force a failure after init
349
+ const spy = vi.spyOn(nobleBackend, 'signSchnorr')
350
+ .mockImplementation(() => { throw new Error('Schnorr signing failed'); });
351
+
352
+ const keyPair: ParallelSignerKeyPair = {
353
+ publicKey: getTestPublicKey(),
354
+ getPrivateKey: makeTestPrivateKey,
355
+ };
356
+
357
+ const tasks: SigningTask[] = [
358
+ {
359
+ taskId: 'bad-schnorr-0',
360
+ inputIndex: 0,
361
+ hash: new Uint8Array(32).fill(0x11),
362
+ signatureType: SignatureType.Schnorr,
363
+ sighashType: 0x00,
364
+ },
365
+ ];
366
+
367
+ const result = await pool.signBatch(tasks, keyPair);
368
+
369
+ expect(result.success).toBe(false);
370
+ expect(result.errors.size).toBe(1);
371
+ expect(result.errors.get(0)).toBe('Schnorr signing failed');
372
+
373
+ spy.mockRestore();
374
+ });
375
+
376
+ it('should continue signing after individual task failure', async () => {
377
+ const pool = SequentialSigningPool.getInstance();
378
+
379
+ // Spy on sign to fail only on the second call
380
+ let callCount = 0;
381
+ const originalSign = nobleBackend.sign.bind(nobleBackend);
382
+ const spy = vi.spyOn(nobleBackend, 'sign')
383
+ .mockImplementation((...args) => {
384
+ callCount++;
385
+ if (callCount === 2) {
386
+ throw new Error('Second task failed');
387
+ }
388
+ return originalSign(...args);
389
+ });
390
+
391
+ const keyPair: ParallelSignerKeyPair = {
392
+ publicKey: getTestPublicKey(),
393
+ getPrivateKey: makeTestPrivateKey,
394
+ };
395
+
396
+ const tasks: SigningTask[] = [
397
+ {
398
+ taskId: 'task-0',
399
+ inputIndex: 0,
400
+ hash: new Uint8Array(32).fill(0x01),
401
+ signatureType: SignatureType.ECDSA,
402
+ sighashType: 0x01,
403
+ },
404
+ {
405
+ taskId: 'task-1',
406
+ inputIndex: 1,
407
+ hash: new Uint8Array(32).fill(0x02),
408
+ signatureType: SignatureType.ECDSA,
409
+ sighashType: 0x01,
410
+ },
411
+ {
412
+ taskId: 'task-2',
413
+ inputIndex: 2,
414
+ hash: new Uint8Array(32).fill(0x03),
415
+ signatureType: SignatureType.ECDSA,
416
+ sighashType: 0x01,
417
+ },
418
+ ];
419
+
420
+ const result = await pool.signBatch(tasks, keyPair);
421
+
422
+ expect(result.success).toBe(false);
423
+ expect(result.signatures.size).toBe(2); // Tasks 0 and 2 succeeded
424
+ expect(result.errors.size).toBe(1); // Task 1 failed
425
+ expect(result.errors.get(1)).toBe('Second task failed');
426
+
427
+ spy.mockRestore();
428
+ });
429
+
430
+ it('should sign all tasks when key is valid', async () => {
431
+ const pool = SequentialSigningPool.getInstance();
432
+
433
+ const keyPair: ParallelSignerKeyPair = {
434
+ publicKey: getTestPublicKey(),
435
+ getPrivateKey: makeTestPrivateKey,
436
+ };
437
+
438
+ const tasks: SigningTask[] = [
439
+ {
440
+ taskId: 'task-0',
441
+ inputIndex: 0,
442
+ hash: new Uint8Array(32).fill(0x01),
443
+ signatureType: SignatureType.ECDSA,
444
+ sighashType: 0x01,
445
+ },
446
+ {
447
+ taskId: 'task-1',
448
+ inputIndex: 1,
449
+ hash: new Uint8Array(32).fill(0x02),
450
+ signatureType: SignatureType.ECDSA,
451
+ sighashType: 0x01,
452
+ },
453
+ {
454
+ taskId: 'task-2',
455
+ inputIndex: 2,
456
+ hash: new Uint8Array(32).fill(0x03),
457
+ signatureType: SignatureType.ECDSA,
458
+ sighashType: 0x01,
459
+ },
460
+ ];
461
+
462
+ const result = await pool.signBatch(tasks, keyPair);
463
+
464
+ expect(result.success).toBe(true);
465
+ expect(result.signatures.size).toBe(3);
466
+ expect(result.errors.size).toBe(0);
467
+ });
468
+ });
469
+
470
+ describe('Key Security', () => {
471
+ it('should zero private key after signing', async () => {
472
+ const pool = SequentialSigningPool.getInstance();
473
+
474
+ let capturedKey: Uint8Array | null = null;
475
+ const keyPair: ParallelSignerKeyPair = {
476
+ publicKey: getTestPublicKey(),
477
+ getPrivateKey: () => {
478
+ capturedKey = makeTestPrivateKey();
479
+ return capturedKey;
480
+ },
481
+ };
482
+
483
+ const tasks: SigningTask[] = [
484
+ {
485
+ taskId: 'task-0',
486
+ inputIndex: 0,
487
+ hash: new Uint8Array(32).fill(0x11),
488
+ signatureType: SignatureType.ECDSA,
489
+ sighashType: 0x01,
490
+ },
491
+ ];
492
+
493
+ await pool.signBatch(tasks, keyPair);
494
+
495
+ expect(capturedKey).not.toBeNull();
496
+ expect(capturedKey!.every((b) => b === 0)).toBe(true);
497
+ });
498
+
499
+ it('should zero private key even when signing fails', async () => {
500
+ const pool = SequentialSigningPool.getInstance();
501
+
502
+ let capturedKey: Uint8Array | null = null;
503
+ const keyPair: ParallelSignerKeyPair = {
504
+ publicKey: new Uint8Array(33).fill(0x02),
505
+ getPrivateKey: () => {
506
+ // Invalid key (all zeros) will cause signing to fail
507
+ capturedKey = new Uint8Array(32);
508
+ return capturedKey;
509
+ },
510
+ };
511
+
512
+ const tasks: SigningTask[] = [
513
+ {
514
+ taskId: 'task-0',
515
+ inputIndex: 0,
516
+ hash: new Uint8Array(32).fill(0x11),
517
+ signatureType: SignatureType.ECDSA,
518
+ sighashType: 0x01,
519
+ },
520
+ ];
521
+
522
+ await pool.signBatch(tasks, keyPair);
523
+
524
+ expect(capturedKey).not.toBeNull();
525
+ expect(capturedKey!.every((b) => b === 0)).toBe(true);
526
+ });
527
+
528
+ it('should call getPrivateKey once per batch', async () => {
529
+ const pool = SequentialSigningPool.getInstance();
530
+
531
+ let callCount = 0;
532
+ const keyPair: ParallelSignerKeyPair = {
533
+ publicKey: getTestPublicKey(),
534
+ getPrivateKey: () => {
535
+ callCount++;
536
+ return makeTestPrivateKey();
537
+ },
538
+ };
539
+
540
+ const tasks: SigningTask[] = [
541
+ {
542
+ taskId: 'task-0',
543
+ inputIndex: 0,
544
+ hash: new Uint8Array(32).fill(0x11),
545
+ signatureType: SignatureType.ECDSA,
546
+ sighashType: 0x01,
547
+ },
548
+ {
549
+ taskId: 'task-1',
550
+ inputIndex: 1,
551
+ hash: new Uint8Array(32).fill(0x22),
552
+ signatureType: SignatureType.Schnorr,
553
+ sighashType: 0x00,
554
+ },
555
+ ];
556
+
557
+ await pool.signBatch(tasks, keyPair);
558
+
559
+ expect(callCount).toBe(1);
560
+ });
561
+ });
562
+
563
+ describe('Symbol.asyncDispose', () => {
564
+ it('should have Symbol.asyncDispose method', () => {
565
+ const pool = SequentialSigningPool.getInstance();
566
+ expect(typeof pool[Symbol.asyncDispose]).toBe('function');
567
+ });
568
+
569
+ it('should work with await using syntax', async () => {
570
+ SequentialSigningPool.resetInstance();
571
+
572
+ let poolRef: SequentialSigningPool | undefined;
573
+
574
+ {
575
+ await using pool = SequentialSigningPool.getInstance();
576
+ await pool.initialize();
577
+ poolRef = pool;
578
+ expect(poolRef).toBeDefined();
579
+ }
580
+
581
+ // After scope exit, dispose has been called (no-op for sequential)
582
+ expect(poolRef).toBeDefined();
583
+ });
584
+
585
+ it('should be safe to call dispose multiple times', async () => {
586
+ const pool = SequentialSigningPool.getInstance();
587
+ await pool[Symbol.asyncDispose]();
588
+ await pool[Symbol.asyncDispose]();
589
+ // Should not throw
590
+ });
591
+ });
592
+ });
593
+
594
+ describe('detectRuntime for React Native', () => {
595
+ afterEach(() => {
596
+ vi.restoreAllMocks();
597
+ // Clean up navigator mock
598
+ if ('product' in navigator) {
599
+ Object.defineProperty(navigator, 'product', {
600
+ value: '',
601
+ writable: true,
602
+ configurable: true,
603
+ });
604
+ }
605
+ });
606
+
607
+ it('should return react-native when navigator.product is ReactNative', async () => {
608
+ Object.defineProperty(navigator, 'product', {
609
+ value: 'ReactNative',
610
+ writable: true,
611
+ configurable: true,
612
+ });
613
+
614
+ vi.resetModules();
615
+ const { detectRuntime } = await import('../src/workers/index.js');
616
+ expect(detectRuntime()).toBe('react-native');
617
+ });
618
+
619
+ it('should not return react-native for regular environments', async () => {
620
+ Object.defineProperty(navigator, 'product', {
621
+ value: 'Gecko',
622
+ writable: true,
623
+ configurable: true,
624
+ });
625
+
626
+ vi.resetModules();
627
+ const { detectRuntime } = await import('../src/workers/index.js');
628
+ const runtime = detectRuntime();
629
+ expect(runtime).not.toBe('react-native');
630
+ });
631
+ });
632
+
633
+ describe('React Native index entry point', () => {
634
+ it('should export detectRuntime returning react-native', async () => {
635
+ const { detectRuntime } = await import('../src/workers/index.react-native.js');
636
+ expect(detectRuntime()).toBe('react-native');
637
+ });
638
+
639
+ it('should export SequentialSigningPool', async () => {
640
+ const { SequentialSigningPool } = await import('../src/workers/index.react-native.js');
641
+ expect(SequentialSigningPool).toBeDefined();
642
+ expect(typeof SequentialSigningPool.getInstance).toBe('function');
643
+ });
644
+
645
+ it('should export WorkerSigningPool as alias for SequentialSigningPool', async () => {
646
+ const mod = await import('../src/workers/index.react-native.js');
647
+ expect(mod.WorkerSigningPool).toBe(mod.SequentialSigningPool);
648
+ });
649
+
650
+ it('should export SignatureType', async () => {
651
+ const { SignatureType } = await import('../src/workers/index.react-native.js');
652
+ expect(SignatureType.ECDSA).toBe(0);
653
+ expect(SignatureType.Schnorr).toBe(1);
654
+ });
655
+
656
+ it('should export PSBT parallel functions', async () => {
657
+ const { signPsbtParallel, prepareSigningTasks, applySignaturesToPsbt } = await import(
658
+ '../src/workers/index.react-native.js'
659
+ );
660
+ expect(typeof signPsbtParallel).toBe('function');
661
+ expect(typeof prepareSigningTasks).toBe('function');
662
+ expect(typeof applySignaturesToPsbt).toBe('function');
663
+ });
664
+
665
+ it('should export createSigningPool', async () => {
666
+ const { createSigningPool } = await import('../src/workers/index.react-native.js');
667
+ expect(typeof createSigningPool).toBe('function');
668
+ });
669
+ });