@bsv/sdk 2.0.11 → 2.0.13

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 (106) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js +827 -0
  3. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  4. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +654 -0
  5. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  6. package/dist/cjs/src/overlay-tools/HostReputationTracker.js +21 -13
  7. package/dist/cjs/src/overlay-tools/HostReputationTracker.js.map +1 -1
  8. package/dist/cjs/src/primitives/PrivateKey.js +3 -3
  9. package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
  10. package/dist/cjs/src/script/Spend.js +17 -9
  11. package/dist/cjs/src/script/Spend.js.map +1 -1
  12. package/dist/cjs/src/storage/StorageDownloader.js +6 -6
  13. package/dist/cjs/src/storage/StorageDownloader.js.map +1 -1
  14. package/dist/cjs/src/storage/StorageUtils.js +1 -1
  15. package/dist/cjs/src/storage/StorageUtils.js.map +1 -1
  16. package/dist/cjs/src/transaction/MerklePath.js +168 -27
  17. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  18. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  19. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
  20. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  21. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
  22. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  23. package/dist/esm/src/overlay-tools/HostReputationTracker.js +21 -13
  24. package/dist/esm/src/overlay-tools/HostReputationTracker.js.map +1 -1
  25. package/dist/esm/src/primitives/PrivateKey.js +3 -3
  26. package/dist/esm/src/primitives/PrivateKey.js.map +1 -1
  27. package/dist/esm/src/script/Spend.js +17 -9
  28. package/dist/esm/src/script/Spend.js.map +1 -1
  29. package/dist/esm/src/storage/StorageDownloader.js +6 -6
  30. package/dist/esm/src/storage/StorageDownloader.js.map +1 -1
  31. package/dist/esm/src/storage/StorageUtils.js +1 -1
  32. package/dist/esm/src/storage/StorageUtils.js.map +1 -1
  33. package/dist/esm/src/transaction/MerklePath.js +168 -27
  34. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  35. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  36. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
  37. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
  38. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
  39. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
  40. package/dist/types/src/overlay-tools/HostReputationTracker.d.ts.map +1 -1
  41. package/dist/types/src/script/Spend.d.ts.map +1 -1
  42. package/dist/types/src/transaction/MerklePath.d.ts +27 -0
  43. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  44. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  45. package/dist/umd/bundle.js +3 -3
  46. package/dist/umd/bundle.js.map +1 -1
  47. package/docs/reference/storage.md +1 -1
  48. package/docs/reference/transaction.md +40 -0
  49. package/package.json +1 -1
  50. package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
  51. package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
  52. package/src/auth/utils/__tests/validateCertificates.test.ts +12 -9
  53. package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
  54. package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
  55. package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
  56. package/src/kvstore/__tests/LocalKVStore.test.ts +4 -6
  57. package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
  58. package/src/overlay-tools/HostReputationTracker.ts +17 -14
  59. package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
  60. package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
  61. package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
  62. package/src/primitives/PrivateKey.ts +3 -3
  63. package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
  64. package/src/primitives/__tests/Curve.additional.test.ts +208 -0
  65. package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
  66. package/src/primitives/__tests/Hash.additional.test.ts +59 -0
  67. package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
  68. package/src/primitives/__tests/Point.additional.test.ts +503 -0
  69. package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
  70. package/src/primitives/__tests/Random.additional.test.ts +262 -0
  71. package/src/primitives/__tests/Signature.test.ts +333 -0
  72. package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
  73. package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
  74. package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
  75. package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
  76. package/src/script/Spend.ts +19 -11
  77. package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
  78. package/src/script/__tests/Script.additional.test.ts +100 -0
  79. package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
  80. package/src/script/__tests/Spend.additional.test.ts +837 -0
  81. package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
  82. package/src/storage/StorageDownloader.ts +6 -6
  83. package/src/storage/StorageUtils.ts +1 -1
  84. package/src/transaction/MerklePath.ts +196 -36
  85. package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
  86. package/src/transaction/__tests/Broadcaster.test.ts +159 -0
  87. package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
  88. package/src/transaction/__tests/MerklePath.test.ts +232 -21
  89. package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
  90. package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
  91. package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
  92. package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
  93. package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
  94. package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
  95. package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
  96. package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
  97. package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
  98. package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
  99. package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
  100. package/src/wallet/__tests/WERR.test.ts +212 -0
  101. package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
  102. package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
  103. package/src/wallet/__tests/WalletError.test.ts +290 -0
  104. package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
  105. package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
  106. package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
@@ -0,0 +1,105 @@
1
+ import MerklePath from '../../transaction/MerklePath'
2
+
3
+ /**
4
+ * Generate a random 32-byte hex hash.
5
+ */
6
+ function randomHash (): string {
7
+ const bytes = new Uint8Array(32)
8
+ for (let i = 0; i < 32; i++) bytes[i] = Math.floor(Math.random() * 256)
9
+ return Array.from(bytes, b => b.toString(16).padStart(2, '0')).join('')
10
+ }
11
+
12
+ /**
13
+ * Build a full-block compound MerklePath with `count` transactions at level 0.
14
+ * All leaves are txid: true; if count is odd the last leaf gets duplicate: true.
15
+ */
16
+ function buildFullBlockPath (count: number): { mp: MerklePath, txids: string[] } {
17
+ const txids: string[] = []
18
+ const leaves: Array<{ offset: number, hash?: string, txid?: boolean, duplicate?: boolean }> = []
19
+ for (let i = 0; i < count; i++) {
20
+ const h = randomHash()
21
+ txids.push(h)
22
+ leaves.push({ offset: i, hash: h, txid: true })
23
+ }
24
+ if (count % 2 === 1) {
25
+ leaves.push({ offset: count, duplicate: true })
26
+ }
27
+ const mp = new MerklePath(1, [leaves])
28
+ return { mp, txids }
29
+ }
30
+
31
+ /**
32
+ * Pick `n` random items from `arr` without replacement.
33
+ */
34
+ function pickRandom<T> (arr: T[], n: number): T[] {
35
+ const copy = [...arr]
36
+ const result: T[] = []
37
+ for (let i = 0; i < n && copy.length > 0; i++) {
38
+ const idx = Math.floor(Math.random() * copy.length)
39
+ result.push(copy.splice(idx, 1)[0])
40
+ }
41
+ return result
42
+ }
43
+
44
+ describe('MerklePath.extract() benchmarks', () => {
45
+ // Pre-build paths once so construction time is excluded from extract timing.
46
+ let path101: { mp: MerklePath, txids: string[] }
47
+ let path501: { mp: MerklePath, txids: string[] }
48
+ let path999: { mp: MerklePath, txids: string[] }
49
+
50
+ beforeAll(() => {
51
+ path101 = buildFullBlockPath(101)
52
+ path501 = buildFullBlockPath(501)
53
+ path999 = buildFullBlockPath(999)
54
+ })
55
+
56
+ const runBench = (
57
+ label: string,
58
+ getPath: () => { mp: MerklePath, txids: string[] },
59
+ extractCount: number,
60
+ iterations: number = 5
61
+ ): void => {
62
+ it(label, () => {
63
+ const { mp, txids } = getPath()
64
+ const targets = pickRandom(txids, extractCount)
65
+
66
+ const times: number[] = []
67
+ for (let i = 0; i < iterations; i++) {
68
+ const start = performance.now()
69
+ const extracted = mp.extract(targets)
70
+ const elapsed = performance.now() - start
71
+ times.push(elapsed)
72
+ // Correctness check on every iteration
73
+ for (const txid of targets) {
74
+ expect(extracted.computeRoot(txid)).toBe(mp.computeRoot(txid))
75
+ }
76
+ }
77
+
78
+ const avg = times.reduce((a, b) => a + b, 0) / times.length
79
+ const min = Math.min(...times)
80
+ const max = Math.max(...times)
81
+ console.log(
82
+ `[${label}] avg=${avg.toFixed(1)}ms min=${min.toFixed(1)}ms max=${max.toFixed(1)}ms (${iterations} runs)`
83
+ )
84
+ })
85
+ }
86
+
87
+ // --- 101 txids ---
88
+ runBench('101 txids, extract 1', () => path101, 1)
89
+ runBench('101 txids, extract 5', () => path101, 5)
90
+ runBench('101 txids, extract 10', () => path101, 10)
91
+ runBench('101 txids, extract 50', () => path101, 50)
92
+
93
+ // --- 501 txids ---
94
+ runBench('501 txids, extract 1', () => path501, 1)
95
+ runBench('501 txids, extract 5', () => path501, 5)
96
+ runBench('501 txids, extract 10', () => path501, 10)
97
+ runBench('501 txids, extract 50', () => path501, 50)
98
+
99
+ // --- 999 txids ---
100
+ runBench('999 txids, extract 1', () => path999, 1)
101
+ runBench('999 txids, extract 5', () => path999, 5)
102
+ runBench('999 txids, extract 10', () => path999, 10)
103
+ runBench('999 txids, extract 50', () => path999, 50)
104
+ runBench('999 txids, extract 100', () => path999, 100, 3)
105
+ })
@@ -112,6 +112,24 @@ const BRC74JSONTrimmed = {
112
112
  }
113
113
  BRC74JSONTrimmed.path[1] = []
114
114
 
115
+ const BLOCK_125632 = {
116
+ height: 125632,
117
+ merkleroot: '205b2e27c58601fc1a8de04c83b6b0c46f89c16b2161c93441b7e9269cf6bc4a',
118
+ tx: [
119
+ '17cba98da71fe75862aac894392f2ff604356db386767fec364877a5a9ff200c',
120
+ '14ce64bd223ec9bb42662b74fdcf94f96a209a1aee72b7ba7639db503150ec2e',
121
+ '90a2de85351cfadd2326b9b0098e9c453af09b2980835f57a1429bbb44beb872',
122
+ 'a31f2ddfea7ddd4581dca3007ee99e58ea6baa97a8ac3b32bb4610baac9f7206',
123
+ 'c36eeed6fbc0259d30804f59f804dfcda35a54461157d6ac9c094f0ea378f35c',
124
+ '17752483868c52a98407a0e226d73b42e214e0fad548541619d858e1fd4a9549',
125
+ '3b8c4460412cfc55be0d50308ba704a859bd6f83bfed01b0828c9b067cd69246',
126
+ 'a3f1b9d4b3ef3b061af352fdc2d02048417030fef9282c36da689cd899437cdb',
127
+ '66e2b022da877621ef197e02c3ef7d3f820d33a86ead2e72bf966432ea6776f1',
128
+ 'e988b5d7a2cec8e0759ade2e151737d1cdfdde68accff42938583ad12eb98b99',
129
+ '5e7a8a8ec3f912ac1c4e90279c04263f170ed055c0411c8d490b846f01e6a99e'
130
+ ]
131
+ }
132
+
115
133
  const BRC74Root =
116
134
  '57aab6e6fb1b697174ffb64e062c4728f2ffd33ddcfa02a43b64d8cd29b483b4'
117
135
  const BRC74TXID1 =
@@ -200,10 +218,9 @@ describe('MerklePath', () => {
200
218
  it('Serializes and deserializes a combined trimmed path', () => {
201
219
  const [pathA, pathB] = buildSplitPaths()
202
220
  pathA.combine(pathB)
203
- let deserialized: MerklePath
204
- expect(() => { deserialized = MerklePath.fromHex(pathA.toHex()) }).not.toThrow()
205
- expect(deserialized!.computeRoot(BRC74TXID2)).toEqual(BRC74Root)
206
- expect(deserialized!.computeRoot(BRC74TXID3)).toEqual(BRC74Root)
221
+ const deserialized = MerklePath.fromHex(pathA.toHex())
222
+ expect(deserialized.computeRoot(BRC74TXID2)).toEqual(BRC74Root)
223
+ expect(deserialized.computeRoot(BRC74TXID3)).toEqual(BRC74Root)
207
224
  })
208
225
  it('Constructs a compound path from all txids at level 0 only', () => {
209
226
  // A single-level compound path: all txids for a block given at level 0, no higher levels.
@@ -213,24 +230,20 @@ describe('MerklePath', () => {
213
230
  const tx2 = 'cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc'
214
231
  const tx3 = 'dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd'
215
232
  const root4 = merkleHash(merkleHash(tx3 + tx2) + merkleHash(tx1 + tx0))
216
- let mp: MerklePath
217
- expect(() => {
218
- mp = new MerklePath(100, [[
219
- { offset: 0, txid: true, hash: tx0 },
220
- { offset: 1, txid: true, hash: tx1 },
221
- { offset: 2, txid: true, hash: tx2 },
222
- { offset: 3, txid: true, hash: tx3 }
223
- ]])
224
- }).not.toThrow()
225
- expect(mp!.computeRoot(tx0)).toEqual(root4)
226
- expect(mp!.computeRoot(tx1)).toEqual(root4)
227
- expect(mp!.computeRoot(tx2)).toEqual(root4)
228
- expect(mp!.computeRoot(tx3)).toEqual(root4)
233
+ const mp = new MerklePath(100, [[
234
+ { offset: 0, txid: true, hash: tx0 },
235
+ { offset: 1, txid: true, hash: tx1 },
236
+ { offset: 2, txid: true, hash: tx2 },
237
+ { offset: 3, txid: true, hash: tx3 }
238
+ ]])
239
+ expect(mp.computeRoot(tx0)).toEqual(root4)
240
+ expect(mp.computeRoot(tx1)).toEqual(root4)
241
+ expect(mp.computeRoot(tx2)).toEqual(root4)
242
+ expect(mp.computeRoot(tx3)).toEqual(root4)
229
243
  // Serializing and deserializing a single-level compound path should also work
230
- let deserialized: MerklePath
231
- expect(() => { deserialized = MerklePath.fromHex(mp!.toHex()) }).not.toThrow()
232
- expect(deserialized!.computeRoot(tx0)).toEqual(root4)
233
- expect(deserialized!.computeRoot(tx3)).toEqual(root4)
244
+ const deserialized = MerklePath.fromHex(mp.toHex())
245
+ expect(deserialized.computeRoot(tx0)).toEqual(root4)
246
+ expect(deserialized.computeRoot(tx3)).toEqual(root4)
234
247
  })
235
248
  it('Rejects invalid bumps', () => {
236
249
  for (const invalid of invalidBumps) {
@@ -284,4 +297,202 @@ describe('MerklePath', () => {
284
297
  )
285
298
  expect(isValid).toBe(false)
286
299
  })
300
+ it('constructs a compound MerklePath from all txids in a block with odd tree levels', () => {
301
+ const { height, merkleroot, tx } = BLOCK_125632
302
+ const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
303
+ if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
304
+ const mp = new MerklePath(height, [leafs])
305
+ expect(mp.computeRoot()).toBe(merkleroot)
306
+ })
307
+ it('compound path for 3 txids trims, round-trips through hex, and splits into per-txid proofs', () => {
308
+ const { height, merkleroot, tx } = BLOCK_125632
309
+
310
+ // Precompute the full Merkle tree for block 125632.
311
+ // merkleHash(right + left) matches the SDK's internal hash convention.
312
+ const L1 = [
313
+ merkleHash(tx[1] + tx[0]),
314
+ merkleHash(tx[3] + tx[2]),
315
+ merkleHash(tx[5] + tx[4]),
316
+ merkleHash(tx[7] + tx[6]),
317
+ merkleHash(tx[9] + tx[8]),
318
+ merkleHash(tx[10] + tx[10]) // tx[10] duplicated — odd count at level 0
319
+ ]
320
+ const L2 = [
321
+ merkleHash(L1[1] + L1[0]),
322
+ merkleHash(L1[3] + L1[2]),
323
+ merkleHash(L1[5] + L1[4])
324
+ ]
325
+ const L3 = [
326
+ merkleHash(L2[1] + L2[0]),
327
+ merkleHash(L2[2] + L2[2]) // L2 count = 3 (odd) — last node duplicated
328
+ ]
329
+ expect(merkleHash(L3[1] + L3[0])).toBe(merkleroot)
330
+
331
+ // Build minimal per-txid MerklePaths for tx[2], tx[5], and tx[8].
332
+ // tx[8] exercises the odd-level duplication at level 2 ({offset:3, duplicate:true}).
333
+ const mpTx2 = new MerklePath(height, [
334
+ [{ offset: 2, txid: true, hash: tx[2] }, { offset: 3, hash: tx[3] }],
335
+ [{ offset: 0, hash: L1[0] }],
336
+ [{ offset: 1, hash: L2[1] }],
337
+ [{ offset: 1, hash: L3[1] }]
338
+ ])
339
+ const mpTx5 = new MerklePath(height, [
340
+ [{ offset: 4, hash: tx[4] }, { offset: 5, txid: true, hash: tx[5] }],
341
+ [{ offset: 3, hash: L1[3] }],
342
+ [{ offset: 0, hash: L2[0] }],
343
+ [{ offset: 1, hash: L3[1] }]
344
+ ])
345
+ const mpTx8 = new MerklePath(height, [
346
+ [{ offset: 8, txid: true, hash: tx[8] }, { offset: 9, hash: tx[9] }],
347
+ [{ offset: 5, hash: L1[5] }],
348
+ [{ offset: 3, duplicate: true }], // tx[8] is last odd node at level 2
349
+ [{ offset: 0, hash: L3[0] }]
350
+ ])
351
+ expect(mpTx2.computeRoot(tx[2])).toBe(merkleroot)
352
+ expect(mpTx5.computeRoot(tx[5])).toBe(merkleroot)
353
+ expect(mpTx8.computeRoot(tx[8])).toBe(merkleroot)
354
+
355
+ // Combine into one compound path (combine() trims automatically)
356
+ const compound = new MerklePath(height, mpTx2.path.map(l => [...l]))
357
+ compound.combine(mpTx5)
358
+ compound.combine(mpTx8)
359
+ expect(compound.computeRoot(tx[2])).toBe(merkleroot)
360
+ expect(compound.computeRoot(tx[5])).toBe(merkleroot)
361
+ expect(compound.computeRoot(tx[8])).toBe(merkleroot)
362
+
363
+ // Serialize and deserialize
364
+ const deserialized = MerklePath.fromHex(compound.toHex())
365
+ expect(deserialized.computeRoot(tx[2])).toBe(merkleroot)
366
+ expect(deserialized.computeRoot(tx[5])).toBe(merkleroot)
367
+ expect(deserialized.computeRoot(tx[8])).toBe(merkleroot)
368
+
369
+ // Split the deserialized compound path into standalone per-txid proofs.
370
+ // findOrComputeLeaf reconstructs sibling hashes that were trimmed away.
371
+ const splitProof = (source: MerklePath, txOffset: number, txHash: string): MerklePath => {
372
+ const levels = source.path.map((_, h) => {
373
+ const sibOffset = (txOffset >> h) ^ 1
374
+ if (h === 0) {
375
+ const sib = source.findOrComputeLeaf(0, sibOffset)
376
+ if (sib == null) throw new Error('Missing sibling at level 0')
377
+ return [{ offset: txOffset, txid: true, hash: txHash }, sib].sort((a, b) => a.offset - b.offset)
378
+ }
379
+ const sib = source.findOrComputeLeaf(h, sibOffset)
380
+ return sib == null ? [] : [sib]
381
+ })
382
+ return new MerklePath(source.blockHeight, levels)
383
+ }
384
+
385
+ const splitTx2 = splitProof(deserialized, 2, tx[2])
386
+ const splitTx5 = splitProof(deserialized, 5, tx[5])
387
+ const splitTx8 = splitProof(deserialized, 8, tx[8])
388
+
389
+ // Each standalone proof computes the same root — no data was lost through the pipeline
390
+ expect(splitTx2.computeRoot(tx[2])).toBe(merkleroot)
391
+ expect(splitTx5.computeRoot(tx[5])).toBe(merkleroot)
392
+ expect(splitTx8.computeRoot(tx[8])).toBe(merkleroot)
393
+ })
394
+ describe('extract()', () => {
395
+ it('extracts a single-txid proof from a full block compound path', () => {
396
+ const { height, merkleroot, tx } = BLOCK_125632
397
+ const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
398
+ if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
399
+ const fullBlock = new MerklePath(height, [leafs])
400
+
401
+ const extracted = fullBlock.extract([tx[2]])
402
+ expect(extracted.computeRoot(tx[2])).toBe(merkleroot)
403
+ })
404
+
405
+ it('extracts a multi-txid compound proof from a full block compound path', () => {
406
+ const { height, merkleroot, tx } = BLOCK_125632
407
+ const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
408
+ if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
409
+ const fullBlock = new MerklePath(height, [leafs])
410
+
411
+ const extracted = fullBlock.extract([tx[2], tx[5], tx[8]])
412
+ expect(extracted.computeRoot(tx[2])).toBe(merkleroot)
413
+ expect(extracted.computeRoot(tx[5])).toBe(merkleroot)
414
+ expect(extracted.computeRoot(tx[8])).toBe(merkleroot)
415
+ })
416
+
417
+ it('extracted path serializes and deserializes correctly', () => {
418
+ const { height, merkleroot, tx } = BLOCK_125632
419
+ const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
420
+ if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
421
+ const fullBlock = new MerklePath(height, [leafs])
422
+
423
+ const extracted = fullBlock.extract([tx[2], tx[8]])
424
+ const roundTripped = MerklePath.fromHex(extracted.toHex())
425
+ expect(roundTripped.computeRoot(tx[2])).toBe(merkleroot)
426
+ expect(roundTripped.computeRoot(tx[8])).toBe(merkleroot)
427
+ })
428
+
429
+ it('extracted path is smaller than the full block path', () => {
430
+ const { height, tx } = BLOCK_125632
431
+ const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
432
+ if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
433
+ const fullBlock = new MerklePath(height, [leafs])
434
+
435
+ const extracted = fullBlock.extract([tx[2], tx[5]])
436
+ expect(extracted.toBinary().length).toBeLessThan(fullBlock.toBinary().length)
437
+ })
438
+
439
+ it('extract from a trimmed multi-level compound path also works', () => {
440
+ const { height, merkleroot, tx } = BLOCK_125632
441
+ const [pathA, pathB] = buildSplitPaths()
442
+ pathA.combine(pathB)
443
+ // pathA is now a trimmed multi-level compound path for tx[2] and tx[3]
444
+ const txid2 = BRC74TXID2
445
+ const compound = new MerklePath(BRC74JSON.blockHeight, BRC74JSON.path)
446
+ const extracted = compound.extract([txid2])
447
+ expect(extracted.computeRoot(txid2)).toBe(compound.computeRoot(txid2))
448
+ // BLOCK_125632 variant
449
+ const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
450
+ if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
451
+ const fullBlock = new MerklePath(height, [leafs])
452
+ const ex = fullBlock.extract([tx[0], tx[10]])
453
+ expect(ex.computeRoot(tx[0])).toBe(merkleroot)
454
+ expect(ex.computeRoot(tx[10])).toBe(merkleroot)
455
+ })
456
+
457
+ it('throws when no txids are provided', () => {
458
+ const { height, tx } = BLOCK_125632
459
+ const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
460
+ if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
461
+ const fullBlock = new MerklePath(height, [leafs])
462
+ expect(() => fullBlock.extract([])).toThrow('At least one txid must be provided')
463
+ })
464
+
465
+ it('throws when a txid is not in the path', () => {
466
+ const { height, tx } = BLOCK_125632
467
+ const leafs = tx.map((hash, offset) => ({ hash, txid: true, offset }))
468
+ if (leafs.length % 2) leafs.push({ offset: leafs.length, duplicate: true } as any)
469
+ const fullBlock = new MerklePath(height, [leafs])
470
+ expect(() => fullBlock.extract(['deadbeef'.repeat(8)])).toThrow()
471
+ })
472
+ })
473
+
474
+ it('findOrComputeLeaf duplicates leaf0 when leaf1 carries both a hash and duplicate=true', () => {
475
+ // Covers the leaf1.duplicate === true branch inside findOrComputeLeaf.
476
+ // That branch is reached when leaf1.hash is non-null (bypassing the null-check above it)
477
+ // but leaf1.duplicate is also true — an unusual but valid interface state.
478
+ const tx0 = 'aa'.repeat(32)
479
+ const tx1 = 'bb'.repeat(32)
480
+
481
+ // Build a minimal valid path so the constructor does not throw.
482
+ const mp = new MerklePath(1, [[
483
+ { offset: 0, txid: true, hash: tx0 },
484
+ { offset: 1, hash: tx1 }
485
+ ]])
486
+
487
+ // Mutate: give the sibling leaf at offset 1 both a hash and duplicate=true.
488
+ // findOrComputeLeaf(1, 0) will:
489
+ // - not find offset 0 in path[1] (path.length === 1, no higher levels)
490
+ // - recurse to level 0: leaf0 = tx0 (offset 0), leaf1 = {hash:tx1, duplicate:true}
491
+ // - leaf1.hash is non-null → skips the null-branch
492
+ // - leaf1.duplicate === true → line 349: workinghash = hash(leaf0 + leaf0)
493
+ mp.path[0][1] = { offset: 1, hash: tx1, duplicate: true }
494
+
495
+ const result = mp.findOrComputeLeaf(1, 0)
496
+ expect(result?.hash).toBe(merkleHash(tx0 + tx0))
497
+ })
287
498
  })
@@ -0,0 +1,225 @@
1
+ import Transaction from '../Transaction'
2
+ import LockingScript from '../../script/LockingScript'
3
+ import UnlockingScript from '../../script/UnlockingScript'
4
+ import { toArray } from '../../primitives/utils'
5
+
6
+ // Known EF-format transaction hex (BRC-30)
7
+ const KNOWN_EF_HEX =
8
+ '010000000000000000ef01ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff3e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac00000000'
9
+
10
+ // Known BEEF V1 hex (non-Atomic, has no atomicTxid)
11
+ const KNOWN_BEEF_V1_HEX =
12
+ '0100beef01fe636d0c0007021400fe507c0c7aa754cef1f7889d5fd395cf1f785dd7de98eed895dbedfe4e5bc70d1502ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e010b00bc4ff395efd11719b277694cface5aa50d085a0bb81f613f70313acd28cf4557010400574b2d9142b8d28b61d88e3b2c3f44d858411356b49a28a4643b6d1a6a092a5201030051a05fc84d531b5d250c23f4f886f6812f9fe3f402d61607f977b4ecd2701c19010000fd781529d58fc2523cf396a7f25440b409857e7e221766c57214b1d38c7b481f01010062f542f45ea3660f86c013ced80534cb5fd4c19d66c56e7e8c5d4bf2d40acc5e010100b121e91836fd7cd5102b654e9f72f3cf6fdbfd0b161c53a9c54b12c841126331020100000001cd4e4cac3c7b56920d1e7655e7e260d31f29d9a388d04910f1bbd72304a79029010000006b483045022100e75279a205a547c445719420aa3138bf14743e3f42618e5f86a19bde14bb95f7022064777d34776b05d816daf1699493fcdf2ef5a5ab1ad710d9c97bfb5b8f7cef3641210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000001000100000001ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000000'
13
+
14
+ describe('Transaction – additional coverage', () => {
15
+ describe('fromHexEF', () => {
16
+ it('parses a known EF hex string', () => {
17
+ const tx = Transaction.fromHexEF(KNOWN_EF_HEX)
18
+ expect(tx).toBeInstanceOf(Transaction)
19
+ expect(tx.inputs).toHaveLength(1)
20
+ expect(tx.outputs).toHaveLength(1)
21
+ })
22
+ })
23
+
24
+ describe('fromAtomicBEEF – non-atomic BEEF', () => {
25
+ it('throws when passed a regular BEEF with no atomicTxid', () => {
26
+ const beefBytes = toArray(KNOWN_BEEF_V1_HEX, 'hex')
27
+ expect(() => Transaction.fromAtomicBEEF(beefBytes)).toThrow(
28
+ 'beef must conform to BRC-95 and must contain the subject txid.'
29
+ )
30
+ })
31
+ })
32
+
33
+ describe('addInput', () => {
34
+ it('throws when both sourceTXID and sourceTransaction are undefined', () => {
35
+ const tx = new Transaction()
36
+ expect(() =>
37
+ tx.addInput({
38
+ sourceOutputIndex: 0,
39
+ unlockingScript: new UnlockingScript(),
40
+ sequence: 0xffffffff
41
+ })
42
+ ).toThrow('A reference to an an input transaction is required')
43
+ })
44
+
45
+ it('sets sequence to 0xffffffff when not provided', () => {
46
+ const tx = new Transaction()
47
+ tx.addInput({
48
+ sourceTXID: '00'.repeat(32),
49
+ sourceOutputIndex: 0,
50
+ unlockingScript: new UnlockingScript()
51
+ })
52
+ expect(tx.inputs[0].sequence).toBe(0xffffffff)
53
+ })
54
+ })
55
+
56
+ describe('addOutput', () => {
57
+ it('throws when satoshis is undefined and change is not true', () => {
58
+ const tx = new Transaction()
59
+ expect(() =>
60
+ tx.addOutput({
61
+ lockingScript: new LockingScript()
62
+ })
63
+ ).toThrow('either satoshis must be defined or change must be set to true')
64
+ })
65
+
66
+ it('throws when satoshis is negative', () => {
67
+ const tx = new Transaction()
68
+ expect(() =>
69
+ tx.addOutput({
70
+ lockingScript: new LockingScript(),
71
+ satoshis: -1
72
+ })
73
+ ).toThrow('satoshis must be a positive integer or zero')
74
+ })
75
+
76
+ it('throws when lockingScript is null', () => {
77
+ const tx = new Transaction()
78
+ expect(() =>
79
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
80
+ tx.addOutput({ satoshis: 100, lockingScript: null as any })
81
+ ).toThrow('lockingScript must be defined')
82
+ })
83
+ })
84
+
85
+ describe('addP2PKHOutput', () => {
86
+ it('adds a change output when satoshis is omitted', () => {
87
+ const tx = new Transaction()
88
+ // Pass a 20-byte hash directly to avoid base58 parsing
89
+ const pubKeyHash = new Array(20).fill(0x01)
90
+ tx.addP2PKHOutput(pubKeyHash)
91
+ expect(tx.outputs).toHaveLength(1)
92
+ expect(tx.outputs[0].change).toBe(true)
93
+ })
94
+ })
95
+
96
+ describe('hash / id', () => {
97
+ it('returns hex string from hash("hex")', () => {
98
+ const tx = new Transaction()
99
+ const h = tx.hash('hex')
100
+ expect(typeof h).toBe('string')
101
+ expect((h as string)).toHaveLength(64)
102
+ })
103
+
104
+ it('returns binary array from id() without enc', () => {
105
+ const tx = new Transaction()
106
+ const id = tx.id()
107
+ expect(Array.isArray(id)).toBe(true)
108
+ expect(id).toHaveLength(32)
109
+ })
110
+ })
111
+
112
+ describe('toHexAtomicBEEF', () => {
113
+ it('produces a hex string from toHexAtomicBEEF()', () => {
114
+ const sourceTx = new Transaction(
115
+ 1,
116
+ [],
117
+ [{ lockingScript: new LockingScript(), satoshis: 1000 }],
118
+ 0
119
+ )
120
+ const tx = new Transaction(1, [], [{ lockingScript: new LockingScript(), satoshis: 900 }], 0)
121
+ tx.addInput({
122
+ sourceTXID: sourceTx.id('hex'),
123
+ sourceTransaction: sourceTx,
124
+ sourceOutputIndex: 0,
125
+ unlockingScript: new UnlockingScript(),
126
+ sequence: 0xffffffff
127
+ })
128
+ const hex = tx.toHexAtomicBEEF()
129
+ expect(typeof hex).toBe('string')
130
+ expect(hex.length).toBeGreaterThan(0)
131
+ })
132
+ })
133
+
134
+ describe('getFee', () => {
135
+ it('throws when an input has no sourceTransaction', () => {
136
+ const tx = new Transaction()
137
+ tx.addInput({
138
+ sourceTXID: '00'.repeat(32),
139
+ sourceOutputIndex: 0,
140
+ unlockingScript: new UnlockingScript()
141
+ })
142
+ expect(() => tx.getFee()).toThrow(
143
+ 'Source transactions or sourceSatoshis are required for all inputs to calculate fee'
144
+ )
145
+ })
146
+ })
147
+
148
+ describe('sign', () => {
149
+ it('throws when an output has undefined satoshis and change is not set', async () => {
150
+ const tx = new Transaction(
151
+ 1,
152
+ [],
153
+ [{ lockingScript: new LockingScript(), satoshis: undefined, change: false }],
154
+ 0
155
+ )
156
+ await expect(tx.sign()).rejects.toThrow(
157
+ 'One or more transaction outputs is missing an amount'
158
+ )
159
+ })
160
+
161
+ it('throws when an output has undefined satoshis and change is true (uncomputed change)', async () => {
162
+ const tx = new Transaction(
163
+ 1,
164
+ [],
165
+ [{ lockingScript: new LockingScript(), satoshis: undefined, change: true }],
166
+ 0
167
+ )
168
+ await expect(tx.sign()).rejects.toThrow(
169
+ 'There are still change outputs with uncomputed amounts'
170
+ )
171
+ })
172
+ })
173
+
174
+ describe('toEF / writeEF error paths', () => {
175
+ it('throws when an input has no sourceTransaction during EF serialization', () => {
176
+ // sourceTXID is defined so addInput passes, but sourceTransaction is undefined
177
+ const tx = new Transaction()
178
+ tx.addInput({
179
+ sourceTXID: '00'.repeat(32),
180
+ sourceOutputIndex: 0,
181
+ unlockingScript: new UnlockingScript()
182
+ })
183
+ expect(() => tx.toEF()).toThrow(
184
+ 'All inputs must have source transactions when serializing to EF format'
185
+ )
186
+ })
187
+ })
188
+
189
+ describe('toBinary / writeTransactionBody error paths', () => {
190
+ it('throws when an input has no sourceTXID and no sourceTransaction', () => {
191
+ // Bypass addInput validation by constructing directly
192
+ const tx = new Transaction(
193
+ 1,
194
+ [
195
+ {
196
+ sourceOutputIndex: 0,
197
+ unlockingScript: new UnlockingScript(),
198
+ sequence: 0xffffffff
199
+ // no sourceTXID, no sourceTransaction
200
+ }
201
+ ],
202
+ [],
203
+ 0
204
+ )
205
+ expect(() => tx.toBinary()).toThrow('sourceTransaction is undefined')
206
+ })
207
+
208
+ it('throws when an input has no unlockingScript during serialization', () => {
209
+ const tx = new Transaction(
210
+ 1,
211
+ [
212
+ {
213
+ sourceTXID: '00'.repeat(32),
214
+ sourceOutputIndex: 0,
215
+ sequence: 0xffffffff
216
+ // no unlockingScript
217
+ }
218
+ ],
219
+ [],
220
+ 0
221
+ )
222
+ expect(() => tx.toBinary()).toThrow('unlockingScript is undefined')
223
+ })
224
+ })
225
+ })