@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,134 @@
1
+ import RPuzzle from '../RPuzzle'
2
+ import Spend from '../../Spend'
3
+ import LockingScript from '../../LockingScript'
4
+ import Transaction from '../../../transaction/Transaction'
5
+ import PrivateKey from '../../../primitives/PrivateKey'
6
+ import BigNumber from '../../../primitives/BigNumber'
7
+ import Curve from '../../../primitives/Curve'
8
+
9
+ const ZERO_TXID = '0'.repeat(64)
10
+
11
+ function getRValue (k: BigNumber): number[] {
12
+ const c = new Curve()
13
+ let r = c.g.mul(k).x?.umod(c.n)?.toArray()
14
+ if (r == null) return []
15
+ if (r[0] > 127) r = [0, ...r]
16
+ return r
17
+ }
18
+
19
+ async function buildRPuzzleSpend (
20
+ puz: RPuzzle,
21
+ k: BigNumber,
22
+ privateKey: PrivateKey,
23
+ r: number[],
24
+ signOutputs: 'all' | 'none' | 'single' = 'all',
25
+ anyoneCanPay: boolean = false
26
+ ): Promise<Spend> {
27
+ const lockingScript: LockingScript = puz.lock(r)
28
+ const sourceTx = new Transaction(1, [], [{ lockingScript, satoshis: 1 }], 0)
29
+ const spendTx = new Transaction(
30
+ 1,
31
+ [{ sourceTransaction: sourceTx, sourceOutputIndex: 0, sequence: 0xffffffff }],
32
+ [{ lockingScript: LockingScript.fromASM('OP_1'), satoshis: 1 }],
33
+ 0
34
+ )
35
+ const unlockingScript = await puz.unlock(k, privateKey, signOutputs, anyoneCanPay).sign(spendTx, 0)
36
+ return new Spend({
37
+ sourceTXID: sourceTx.id('hex'),
38
+ sourceOutputIndex: 0,
39
+ sourceSatoshis: 1,
40
+ lockingScript,
41
+ transactionVersion: 1,
42
+ otherInputs: [],
43
+ outputs: spendTx.outputs,
44
+ inputIndex: 0,
45
+ unlockingScript,
46
+ inputSequence: 0xffffffff,
47
+ lockTime: 0
48
+ })
49
+ }
50
+
51
+ describe('RPuzzle – additional coverage', () => {
52
+ const k = new BigNumber(12345678)
53
+ const privateKey = new PrivateKey(1)
54
+ const r = getRValue(k)
55
+
56
+ describe('signOutputs variations', () => {
57
+ it('signs with signOutputs=none', async () => {
58
+ const puz = new RPuzzle()
59
+ const spend = await buildRPuzzleSpend(puz, k, privateKey, r, 'none')
60
+ expect(spend.validate()).toBe(true)
61
+ })
62
+
63
+ it('signs with signOutputs=single', async () => {
64
+ const puz = new RPuzzle()
65
+ const spend = await buildRPuzzleSpend(puz, k, privateKey, r, 'single')
66
+ expect(spend.validate()).toBe(true)
67
+ })
68
+
69
+ it('signs with anyoneCanPay=true', async () => {
70
+ const puz = new RPuzzle()
71
+ const spend = await buildRPuzzleSpend(puz, k, privateKey, r, 'all', true)
72
+ expect(spend.validate()).toBe(true)
73
+ })
74
+ })
75
+
76
+ describe('estimateLength', () => {
77
+ it('returns 108', async () => {
78
+ const puz = new RPuzzle()
79
+ const result = await puz.unlock(k, privateKey).estimateLength()
80
+ expect(result).toBe(108)
81
+ })
82
+ })
83
+
84
+ describe('missing source transaction', () => {
85
+ it('throws when input has no sourceTransaction', async () => {
86
+ const puz = new RPuzzle()
87
+ const lockingScript = puz.lock(r)
88
+ // Construct a tx where the input has no sourceTransaction
89
+ const spendTx = new Transaction(
90
+ 1,
91
+ [{ sourceTXID: ZERO_TXID, sourceOutputIndex: 0, sequence: 0xffffffff }],
92
+ [],
93
+ 0
94
+ )
95
+ await expect(
96
+ puz.unlock(k, privateKey).sign(spendTx, 0)
97
+ ).rejects.toThrow('The source transaction is needed')
98
+ })
99
+ })
100
+
101
+ describe('hash type variants', () => {
102
+ it('SHA256 RPuzzle round-trips using hashed r value', async () => {
103
+ const { sha256 } = await import('../../../primitives/Hash')
104
+ const hashedR = sha256(r)
105
+ const puz = new RPuzzle('SHA256')
106
+ const spend = await buildRPuzzleSpend(puz, k, privateKey, hashedR)
107
+ expect(spend.validate()).toBe(true)
108
+ })
109
+
110
+ it('SHA1 RPuzzle round-trips using hashed r value', async () => {
111
+ const { sha1 } = await import('../../../primitives/Hash')
112
+ const hashedR = sha1(r)
113
+ const puz = new RPuzzle('SHA1')
114
+ const spend = await buildRPuzzleSpend(puz, k, privateKey, hashedR)
115
+ expect(spend.validate()).toBe(true)
116
+ })
117
+
118
+ it('RIPEMD160 RPuzzle round-trips using hashed r value', async () => {
119
+ const { ripemd160 } = await import('../../../primitives/Hash')
120
+ const hashedR = ripemd160(r)
121
+ const puz = new RPuzzle('RIPEMD160')
122
+ const spend = await buildRPuzzleSpend(puz, k, privateKey, hashedR)
123
+ expect(spend.validate()).toBe(true)
124
+ })
125
+
126
+ it('HASH160 RPuzzle round-trips using hashed r value', async () => {
127
+ const { hash160 } = await import('../../../primitives/Hash')
128
+ const hashedR = hash160(r)
129
+ const puz = new RPuzzle('HASH160')
130
+ const spend = await buildRPuzzleSpend(puz, k, privateKey, hashedR)
131
+ expect(spend.validate()).toBe(true)
132
+ })
133
+ })
134
+ })
@@ -35,9 +35,9 @@ export class StorageDownloader {
35
35
  }
36
36
  const decodedResults: string[] = []
37
37
  const currentTime = Math.floor(Date.now() / 1000)
38
- for (let i = 0; i < response.outputs.length; i++) {
39
- const tx = Transaction.fromBEEF(response.outputs[i].beef)
40
- const { fields } = PushDrop.decode(tx.outputs[response.outputs[i].outputIndex].lockingScript)
38
+ for (const output of response.outputs) {
39
+ const tx = Transaction.fromBEEF(output.beef)
40
+ const { fields } = PushDrop.decode(tx.outputs[output.outputIndex].lockingScript)
41
41
 
42
42
  const expiryTime = new Utils.Reader(fields[3]).readVarIntNum()
43
43
  if (expiryTime < currentTime) {
@@ -66,10 +66,10 @@ export class StorageDownloader {
66
66
  throw new Error('No one currently hosts this file!')
67
67
  }
68
68
 
69
- for (let i = 0; i < downloadURLs.length; i++) {
69
+ for (const url of downloadURLs) {
70
70
  try {
71
71
  // The url is fetched
72
- const result = await fetch(downloadURLs[i], { method: 'GET' })
72
+ const result = await fetch(url, { method: 'GET' })
73
73
 
74
74
  // If the request fails, continue to the next url
75
75
  if (!result.ok || result.status >= 400 || result.body == null) {
@@ -105,7 +105,7 @@ export class StorageDownloader {
105
105
  data,
106
106
  mimeType: result.headers.get('Content-Type')
107
107
  }
108
- } catch (error) {
108
+ } catch {
109
109
  continue
110
110
  }
111
111
  }
@@ -68,7 +68,7 @@ export const isValidURL = (URL: string): boolean => {
68
68
  try {
69
69
  getHashFromURL(URL)
70
70
  return true
71
- } catch (e) {
71
+ } catch {
72
72
  return false
73
73
  }
74
74
  }
@@ -61,7 +61,7 @@ export default class MerklePath {
61
61
  const blockHeight = reader.readVarIntNum()
62
62
  const treeHeight = reader.readUInt8()
63
63
  // Explicitly define the type of path as an array of arrays of leaf objects
64
- const path: Array<Array<{ offset: number, hash?: string, txid?: boolean, duplicate?: boolean }>> = Array(treeHeight)
64
+ const path: Array<Array<{ offset: number, hash?: string, txid?: boolean, duplicate?: boolean }>> = new Array(treeHeight)
65
65
  .fill(null)
66
66
  .map(() => [])
67
67
  let flags: number, offset: number, nLeavesAtThisHeight: number
@@ -76,7 +76,7 @@ export default class MerklePath {
76
76
  txid?: boolean
77
77
  duplicate?: boolean
78
78
  } = { offset }
79
- if ((flags & 1) !== 0) {
79
+ if ((flags & 1) === 1) {
80
80
  leaf.duplicate = true
81
81
  } else {
82
82
  if ((flags & 2) !== 0) {
@@ -140,7 +140,7 @@ export default class MerklePath {
140
140
  this.path = path
141
141
 
142
142
  // store all of the legal offsets which we expect given the txid indices.
143
- const legalOffsets = Array(this.path.length)
143
+ const legalOffsets = new Array(this.path.length)
144
144
  .fill(0)
145
145
  .map(() => new Set())
146
146
  this.path.forEach((leaves, height) => {
@@ -161,12 +161,10 @@ export default class MerklePath {
161
161
  legalOffsets[h].add((leaf.offset >> h) ^ 1)
162
162
  }
163
163
  }
164
- } else {
165
- if (legalOffsetsOnly && !legalOffsets[height].has(leaf.offset)) {
166
- throw new Error(
167
- `Invalid offset: ${leaf.offset}, at height: ${height}, with legal offsets: ${Array.from(legalOffsets[height]).join(', ')}`
168
- )
169
- }
164
+ } else if (legalOffsetsOnly && !legalOffsets[height].has(leaf.offset)) {
165
+ throw new Error(
166
+ `Invalid offset: ${leaf.offset}, at height: ${height}, with legal offsets: ${Array.from(legalOffsets[height]).join(', ')}`
167
+ )
170
168
  }
171
169
  })
172
170
  })
@@ -260,19 +258,16 @@ export default class MerklePath {
260
258
  computeRoot (txid?: string): string {
261
259
  if (typeof txid !== 'string') {
262
260
  const foundLeaf = this.path[0].find((leaf) => Boolean(leaf?.hash))
263
- if (foundLeaf === null || foundLeaf === undefined) {
261
+ if (foundLeaf == null) {
264
262
  throw new Error('No valid leaf found in the Merkle Path')
265
263
  }
266
264
  txid = foundLeaf.hash
267
265
  }
268
266
  // Find the index of the txid at the lowest level of the Merkle tree
269
267
  if (typeof txid !== 'string') {
270
- throw new Error('Transaction ID is undefined')
268
+ throw new TypeError('Transaction ID is undefined')
271
269
  }
272
270
  const index = this.indexOf(txid)
273
- if (typeof index !== 'number') {
274
- throw new Error(`This proof does not contain the txid: ${txid ?? 'undefined'}`)
275
- }
276
271
  // Calculate the root using the index as a way to determine which direction to concatenate.
277
272
  const hash = (m: string): string =>
278
273
  toHex(hash256(toArray(m, 'hex').reverse()).reverse())
@@ -291,11 +286,16 @@ export default class MerklePath {
291
286
  const offset = (index >> height) ^ 1
292
287
  const leaf = this.findOrComputeLeaf(height, offset)
293
288
  if (typeof leaf !== 'object') {
289
+ // For single-level paths (all txids at level 0), the sibling may be beyond the tree
290
+ // because this is the last odd node at this height. Bitcoin Merkle duplicates it.
291
+ if (this.path.length === 1 && (index >> height) === (maxOffset >> height)) {
292
+ workingHash = hash((workingHash ?? '') + (workingHash ?? ''))
293
+ continue
294
+ }
294
295
  throw new Error(`Missing hash for index ${index} at height ${height}`)
295
- }
296
- if (leaf.duplicate === true) {
296
+ } else if (leaf.duplicate === true) {
297
297
  workingHash = hash((workingHash ?? '') + (workingHash ?? ''))
298
- } else if (offset % 2 !== 0) {
298
+ } else if (offset % 2 === 1) {
299
299
  workingHash = hash((leaf.hash ?? '') + (workingHash ?? ''))
300
300
  } else {
301
301
  workingHash = hash((workingHash ?? '') + (leaf.hash ?? ''))
@@ -334,7 +334,20 @@ export default class MerklePath {
334
334
  if (leaf0 == null || leaf0.hash == null || leaf0.hash === '') return undefined
335
335
 
336
336
  const leaf1 = this.findOrComputeLeaf(h, l + 1)
337
- if (leaf1 == null) return undefined
337
+ if (leaf1?.hash == null) {
338
+ // Explicit duplicate marker — duplicate leaf0 regardless of path depth.
339
+ if (leaf1?.duplicate === true) {
340
+ return { offset, hash: hash(leaf0.hash + leaf0.hash) }
341
+ }
342
+ // For single-level paths, leaf0 may be the last odd node at height h — duplicate it.
343
+ if (this.path.length === 1) {
344
+ const maxOffset0 = this.path[0].reduce((max, lf) => Math.max(max, lf.offset), 0)
345
+ if (l === (maxOffset0 >> h)) {
346
+ return { offset, hash: hash(leaf0.hash + leaf0.hash) }
347
+ }
348
+ }
349
+ return undefined
350
+ }
338
351
 
339
352
  let workinghash: string
340
353
  if (leaf1.duplicate === true) {
@@ -392,26 +405,18 @@ export default class MerklePath {
392
405
  const combinedPath: Array<Array<{ offset: number, hash?: string, txid?: boolean, duplicate?: boolean }>> = []
393
406
  for (let h = 0; h < this.path.length; h++) {
394
407
  combinedPath.push([])
395
- for (let l = 0; l < this.path[h].length; l++) {
396
- combinedPath[h].push(this.path[h][l])
408
+ for (const leaf of this.path[h]) {
409
+ combinedPath[h].push(leaf)
397
410
  }
398
- for (let l = 0; l < other.path[h].length; l++) {
399
- if (
400
- combinedPath[h].find(
401
- (leaf) => leaf.offset === other.path[h][l].offset
402
- ) === undefined
403
- ) {
404
- combinedPath[h].push(other.path[h][l])
405
- } else {
411
+ for (const otherLeaf of other.path[h]) {
412
+ const existingLeaf = combinedPath[h].find(
413
+ (leaf) => leaf.offset === otherLeaf.offset
414
+ )
415
+ if (existingLeaf === undefined) {
416
+ combinedPath[h].push(otherLeaf)
417
+ } else if (otherLeaf?.txid !== undefined && otherLeaf?.txid !== null) {
406
418
  // Ensure that any elements which appear in both are not downgraded to a non txid.
407
- if (other.path[h][l]?.txid !== undefined && other.path[h][l]?.txid !== null) {
408
- const target = combinedPath[h].find(
409
- (leaf) => leaf.offset === other.path[h][l].offset
410
- )
411
- if (target !== null && target !== undefined) {
412
- target.txid = true
413
- }
414
- }
419
+ existingLeaf.txid = true
415
420
  }
416
421
  }
417
422
  }
@@ -477,4 +482,159 @@ export default class MerklePath {
477
482
  dropOffsetsFromLevel(dropOffsets, h)
478
483
  }
479
484
  }
485
+
486
+ /**
487
+ * Cached leaf finder for extract(). Uses Map-based indexes for O(1) lookups
488
+ * and caches computed intermediate hashes to avoid redundant work.
489
+ */
490
+ private cachedFindLeaf (
491
+ height: number,
492
+ offset: number,
493
+ sourceIndex: Array<Map<number, MerklePathLeaf>>,
494
+ hashCache: Map<string, MerklePathLeaf | undefined>,
495
+ maxOffset: number
496
+ ): MerklePathLeaf | undefined {
497
+ const key = `${height}:${offset}`
498
+ if (hashCache.has(key)) return hashCache.get(key)
499
+
500
+ const doHash = (m: string): string =>
501
+ toHex(hash256(toArray(m, 'hex').reverse()).reverse())
502
+
503
+ let leaf: MerklePathLeaf | undefined = height < sourceIndex.length
504
+ ? sourceIndex[height].get(offset)
505
+ : undefined
506
+
507
+ if (leaf != null) {
508
+ hashCache.set(key, leaf)
509
+ return leaf
510
+ }
511
+
512
+ if (height === 0) {
513
+ hashCache.set(key, undefined)
514
+ return undefined
515
+ }
516
+
517
+ const h = height - 1
518
+ const l = offset << 1
519
+ const leaf0 = this.cachedFindLeaf(h, l, sourceIndex, hashCache, maxOffset)
520
+ if (leaf0?.hash == null || leaf0.hash === '') {
521
+ hashCache.set(key, undefined)
522
+ return undefined
523
+ }
524
+
525
+ const leaf1 = this.cachedFindLeaf(h, l + 1, sourceIndex, hashCache, maxOffset)
526
+ if (leaf1?.hash == null) {
527
+ if (leaf1?.duplicate === true || (this.path.length === 1 && l === (maxOffset >> h))) {
528
+ leaf = { offset, hash: doHash(leaf0.hash + leaf0.hash) }
529
+ hashCache.set(key, leaf)
530
+ return leaf
531
+ }
532
+ hashCache.set(key, undefined)
533
+ return undefined
534
+ }
535
+
536
+ const workinghash = leaf1.duplicate === true
537
+ ? doHash(leaf0.hash + leaf0.hash)
538
+ : doHash((leaf1.hash ?? '') + (leaf0.hash ?? ''))
539
+ leaf = { offset, hash: workinghash }
540
+ hashCache.set(key, leaf)
541
+ return leaf
542
+ }
543
+
544
+ /**
545
+ * Extracts a minimal compound MerklePath covering only the specified transaction IDs.
546
+ *
547
+ * Given a compound MerklePath (e.g. all block txids at level 0, or a trimmed
548
+ * compound path), this method reconstructs the sibling hashes at each tree level
549
+ * for every requested txid using cached Map-indexed lookups, then assembles them
550
+ * into a single trimmed compound path.
551
+ *
552
+ * The extracted path is verified to compute the same Merkle root as the source.
553
+ *
554
+ * @param {string[]} txids - Transaction IDs to extract proofs for.
555
+ * @returns {MerklePath} - A new trimmed compound MerklePath covering only the requested txids.
556
+ * @throws {Error} - If no txids are provided, a txid is not found, or the roots do not match.
557
+ *
558
+ * @example
559
+ * // Full block compound path (all txids at level 0)
560
+ * const fullBlock = new MerklePath(height, [allTxidsAtLevel0])
561
+ * // Extract a smaller compound proof covering just two transactions
562
+ * const twoTxProof = fullBlock.extract([txid1, txid2])
563
+ * twoTxProof.computeRoot(txid1) // === fullBlock.computeRoot()
564
+ */
565
+ extract (txids: string[]): MerklePath {
566
+ if (txids.length === 0) {
567
+ throw new Error('At least one txid must be provided to extract')
568
+ }
569
+
570
+ const originalRoot = this.computeRoot()
571
+ const maxOffset = this.path[0].reduce((max, l) => Math.max(max, l.offset), 0)
572
+ const treeHeight = Math.max(this.path.length, 32 - Math.clz32(maxOffset))
573
+
574
+ // Build O(1) lookup indexes for the source path
575
+ const sourceIndex: Array<Map<number, MerklePathLeaf>> = new Array(this.path.length)
576
+ for (let h = 0; h < this.path.length; h++) {
577
+ const map = new Map<number, MerklePathLeaf>()
578
+ for (const leaf of this.path[h]) map.set(leaf.offset, leaf)
579
+ sourceIndex[h] = map
580
+ }
581
+
582
+ const hashCache = new Map<string, MerklePathLeaf | undefined>()
583
+
584
+ // Build txid-to-offset index for O(1) lookup
585
+ const txidToOffset = new Map<string, number>()
586
+ for (const leaf of this.path[0]) {
587
+ if (leaf.hash != null) txidToOffset.set(leaf.hash, leaf.offset)
588
+ }
589
+
590
+ // Collect all needed leaves per level
591
+ const neededPerLevel: Array<Map<number, MerklePathLeaf>> = new Array(treeHeight)
592
+ for (let h = 0; h < treeHeight; h++) neededPerLevel[h] = new Map()
593
+
594
+ for (const txid of txids) {
595
+ const txOffset = txidToOffset.get(txid)
596
+ if (txOffset === undefined) {
597
+ throw new Error(`Transaction ID ${txid} not found in the Merkle Path`)
598
+ }
599
+
600
+ // Level 0: the txid leaf + its sibling
601
+ neededPerLevel[0].set(txOffset, { offset: txOffset, txid: true, hash: txid })
602
+ const sib0Offset = txOffset ^ 1
603
+ if (!neededPerLevel[0].has(sib0Offset)) {
604
+ const sib = this.cachedFindLeaf(0, sib0Offset, sourceIndex, hashCache, maxOffset)
605
+ if (sib != null) neededPerLevel[0].set(sib0Offset, sib)
606
+ }
607
+
608
+ // Higher levels: just the sibling at each height
609
+ for (let h = 1; h < treeHeight; h++) {
610
+ const sibOffset = (txOffset >> h) ^ 1
611
+ if (neededPerLevel[h].has(sibOffset)) continue
612
+ const sib = this.cachedFindLeaf(h, sibOffset, sourceIndex, hashCache, maxOffset)
613
+ if (sib != null) {
614
+ neededPerLevel[h].set(sibOffset, sib)
615
+ } else if ((txOffset >> h) === (maxOffset >> h)) {
616
+ neededPerLevel[h].set(sibOffset, { offset: sibOffset, duplicate: true })
617
+ }
618
+ }
619
+ }
620
+
621
+ // Build sorted compound path
622
+ const compoundPath: Array<Array<{ offset: number, hash?: string, txid?: boolean, duplicate?: boolean }>> = new Array(treeHeight)
623
+ for (let h = 0; h < treeHeight; h++) {
624
+ compoundPath[h] = Array.from(neededPerLevel[h].values())
625
+ .sort((a, b) => a.offset - b.offset)
626
+ }
627
+
628
+ const compound = new MerklePath(this.blockHeight, compoundPath)
629
+ compound.trim()
630
+
631
+ const extractedRoot = compound.computeRoot()
632
+ if (extractedRoot !== originalRoot) {
633
+ throw new Error(
634
+ `Extracted path root ${extractedRoot} does not match original root ${originalRoot}`
635
+ )
636
+ }
637
+
638
+ return compound
639
+ }
480
640
  }
@@ -0,0 +1,22 @@
1
+ import BeefParty from '../BeefParty'
2
+ import { Beef } from '../Beef'
3
+
4
+ describe('BeefParty – additional coverage', () => {
5
+ describe('mergeBeefFromParty', () => {
6
+ it('merges a Beef object directly (non-array branch)', () => {
7
+ const bp = new BeefParty(['alice'])
8
+ const b = new Beef()
9
+ bp.mergeBeefFromParty('alice', b)
10
+ // No error thrown means the Beef object branch executed
11
+ expect(bp.isParty('alice')).toBe(true)
12
+ })
13
+
14
+ it('merges a binary Beef (array branch) via Beef.fromBinary', () => {
15
+ const bp = new BeefParty(['bob'])
16
+ const emptyBeef = new Beef()
17
+ const binary = emptyBeef.toBinary()
18
+ bp.mergeBeefFromParty('bob', binary)
19
+ expect(bp.isParty('bob')).toBe(true)
20
+ })
21
+ })
22
+ })
@@ -0,0 +1,159 @@
1
+ import {
2
+ BroadcastResponse,
3
+ BroadcastFailure,
4
+ isBroadcastResponse,
5
+ isBroadcastFailure
6
+ } from '../../transaction/Broadcaster'
7
+
8
+ // Broadcaster.ts contains two interfaces (BroadcastResponse, BroadcastFailure, Broadcaster)
9
+ // and two exported type-guard functions. The interfaces have no runtime representation,
10
+ // so coverage comes entirely from exercising isBroadcastResponse and isBroadcastFailure.
11
+
12
+ describe('isBroadcastResponse', () => {
13
+ it('returns true for an object with status "success"', () => {
14
+ const r: BroadcastResponse = {
15
+ status: 'success',
16
+ txid: 'abc123',
17
+ message: 'broadcast successful'
18
+ }
19
+ expect(isBroadcastResponse(r)).toBe(true)
20
+ })
21
+
22
+ it('returns false for an object with status "error"', () => {
23
+ const r: BroadcastFailure = {
24
+ status: 'error',
25
+ code: '500',
26
+ description: 'Internal Server Error'
27
+ }
28
+ expect(isBroadcastResponse(r)).toBe(false)
29
+ })
30
+
31
+ it('narrows the type to BroadcastResponse inside a conditional', () => {
32
+ const r: BroadcastResponse | BroadcastFailure = {
33
+ status: 'success',
34
+ txid: 'deadbeef',
35
+ message: 'ok',
36
+ competingTxs: ['aabbcc']
37
+ }
38
+ if (isBroadcastResponse(r)) {
39
+ expect(r.txid).toBe('deadbeef')
40
+ expect(r.message).toBe('ok')
41
+ expect(r.competingTxs).toEqual(['aabbcc'])
42
+ } else {
43
+ fail('Expected isBroadcastResponse to return true')
44
+ }
45
+ })
46
+
47
+ it('returns false even when the failure object has extra fields', () => {
48
+ const r: BroadcastFailure = {
49
+ status: 'error',
50
+ code: '404',
51
+ txid: 'txidfail',
52
+ description: 'Not found',
53
+ more: { detail: 'extra' }
54
+ }
55
+ expect(isBroadcastResponse(r)).toBe(false)
56
+ })
57
+ })
58
+
59
+ describe('isBroadcastFailure', () => {
60
+ it('returns true for an object with status "error"', () => {
61
+ const r: BroadcastFailure = {
62
+ status: 'error',
63
+ code: 'ERR_UNKNOWN',
64
+ description: 'Something went wrong'
65
+ }
66
+ expect(isBroadcastFailure(r)).toBe(true)
67
+ })
68
+
69
+ it('returns false for an object with status "success"', () => {
70
+ const r: BroadcastResponse = {
71
+ status: 'success',
72
+ txid: 'txid1',
73
+ message: 'done'
74
+ }
75
+ expect(isBroadcastFailure(r)).toBe(false)
76
+ })
77
+
78
+ it('narrows the type to BroadcastFailure inside a conditional', () => {
79
+ const r: BroadcastResponse | BroadcastFailure = {
80
+ status: 'error',
81
+ code: '503',
82
+ txid: 'tx503',
83
+ description: 'Service unavailable',
84
+ more: { raw: 'body' }
85
+ }
86
+ if (isBroadcastFailure(r)) {
87
+ expect(r.code).toBe('503')
88
+ expect(r.description).toBe('Service unavailable')
89
+ expect(r.txid).toBe('tx503')
90
+ expect(r.more).toEqual({ raw: 'body' })
91
+ } else {
92
+ fail('Expected isBroadcastFailure to return true')
93
+ }
94
+ })
95
+
96
+ it('returns false even when the success object has an optional competingTxs field', () => {
97
+ const r: BroadcastResponse = {
98
+ status: 'success',
99
+ txid: 't1',
100
+ message: 'm1',
101
+ competingTxs: ['other']
102
+ }
103
+ expect(isBroadcastFailure(r)).toBe(false)
104
+ })
105
+
106
+ it('handles BroadcastFailure with only the required fields', () => {
107
+ const r: BroadcastFailure = {
108
+ status: 'error',
109
+ code: '400',
110
+ description: 'Bad request'
111
+ }
112
+ expect(isBroadcastFailure(r)).toBe(true)
113
+ if (isBroadcastFailure(r)) {
114
+ expect(r.txid).toBeUndefined()
115
+ expect(r.more).toBeUndefined()
116
+ }
117
+ })
118
+ })
119
+
120
+ describe('BroadcastResponse interface shape', () => {
121
+ it('accepts competingTxs as an optional field', () => {
122
+ const withCompeting: BroadcastResponse = {
123
+ status: 'success',
124
+ txid: 'tx',
125
+ message: 'msg',
126
+ competingTxs: ['tx1', 'tx2']
127
+ }
128
+ expect(withCompeting.competingTxs).toHaveLength(2)
129
+
130
+ const withoutCompeting: BroadcastResponse = {
131
+ status: 'success',
132
+ txid: 'tx',
133
+ message: 'msg'
134
+ }
135
+ expect(withoutCompeting.competingTxs).toBeUndefined()
136
+ })
137
+ })
138
+
139
+ describe('BroadcastFailure interface shape', () => {
140
+ it('accepts txid and more as optional fields', () => {
141
+ const full: BroadcastFailure = {
142
+ status: 'error',
143
+ code: '422',
144
+ txid: 'txfull',
145
+ description: 'Unprocessable',
146
+ more: { hints: ['check input'] }
147
+ }
148
+ expect(full.txid).toBe('txfull')
149
+ expect(full.more).toEqual({ hints: ['check input'] })
150
+
151
+ const minimal: BroadcastFailure = {
152
+ status: 'error',
153
+ code: '422',
154
+ description: 'Unprocessable'
155
+ }
156
+ expect(minimal.txid).toBeUndefined()
157
+ expect(minimal.more).toBeUndefined()
158
+ })
159
+ })