@bsv/sdk 2.0.11 → 2.0.12

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 (43) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/overlay-tools/HostReputationTracker.js +21 -13
  3. package/dist/cjs/src/overlay-tools/HostReputationTracker.js.map +1 -1
  4. package/dist/cjs/src/primitives/PrivateKey.js +3 -3
  5. package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
  6. package/dist/cjs/src/script/Spend.js +17 -9
  7. package/dist/cjs/src/script/Spend.js.map +1 -1
  8. package/dist/cjs/src/storage/StorageDownloader.js +6 -6
  9. package/dist/cjs/src/storage/StorageDownloader.js.map +1 -1
  10. package/dist/cjs/src/storage/StorageUtils.js +1 -1
  11. package/dist/cjs/src/storage/StorageUtils.js.map +1 -1
  12. package/dist/cjs/src/transaction/MerklePath.js +36 -27
  13. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  14. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  15. package/dist/esm/src/overlay-tools/HostReputationTracker.js +21 -13
  16. package/dist/esm/src/overlay-tools/HostReputationTracker.js.map +1 -1
  17. package/dist/esm/src/primitives/PrivateKey.js +3 -3
  18. package/dist/esm/src/primitives/PrivateKey.js.map +1 -1
  19. package/dist/esm/src/script/Spend.js +17 -9
  20. package/dist/esm/src/script/Spend.js.map +1 -1
  21. package/dist/esm/src/storage/StorageDownloader.js +6 -6
  22. package/dist/esm/src/storage/StorageDownloader.js.map +1 -1
  23. package/dist/esm/src/storage/StorageUtils.js +1 -1
  24. package/dist/esm/src/storage/StorageUtils.js.map +1 -1
  25. package/dist/esm/src/transaction/MerklePath.js +36 -27
  26. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  27. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  28. package/dist/types/src/overlay-tools/HostReputationTracker.d.ts.map +1 -1
  29. package/dist/types/src/script/Spend.d.ts.map +1 -1
  30. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  31. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  32. package/dist/umd/bundle.js +3 -3
  33. package/dist/umd/bundle.js.map +1 -1
  34. package/package.json +1 -1
  35. package/src/auth/utils/__tests/validateCertificates.test.ts +12 -9
  36. package/src/kvstore/__tests/LocalKVStore.test.ts +4 -6
  37. package/src/overlay-tools/HostReputationTracker.ts +17 -14
  38. package/src/primitives/PrivateKey.ts +3 -3
  39. package/src/script/Spend.ts +19 -11
  40. package/src/storage/StorageDownloader.ts +6 -6
  41. package/src/storage/StorageUtils.ts +1 -1
  42. package/src/transaction/MerklePath.ts +41 -36
  43. package/src/transaction/__tests/MerklePath.test.ts +152 -21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/sdk",
3
- "version": "2.0.11",
3
+ "version": "2.0.12",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Software Development Kit",
6
6
  "main": "dist/cjs/mod.js",
@@ -1,7 +1,8 @@
1
1
  import { validateCertificates } from '../../../auth/utils/validateCertificates'
2
2
  import { VerifiableCertificate } from '../../../auth/certificates/VerifiableCertificate'
3
- import { ProtoWallet } from '../../../wallet/index'
3
+ import { ProtoWallet, WalletInterface } from '../../../wallet/index'
4
4
  import { PrivateKey } from '../../../primitives/index'
5
+ import { AuthMessage } from '../../../auth/types'
5
6
 
6
7
  let mockVerify = jest.fn(async () => await Promise.resolve(true))
7
8
  let mockDecryptFields = jest.fn(
@@ -40,8 +41,8 @@ jest.mock('../../../auth/certificates/VerifiableCertificate', () => {
40
41
  })
41
42
 
42
43
  describe('validateCertificates', () => {
43
- let verifierWallet
44
- let message
44
+ let verifierWallet: WalletInterface
45
+ let message: AuthMessage
45
46
 
46
47
  beforeEach(() => {
47
48
  jest.clearAllMocks()
@@ -53,8 +54,10 @@ describe('validateCertificates', () => {
53
54
  async () => await Promise.resolve({ field1: 'decryptedValue1' })
54
55
  )
55
56
 
56
- verifierWallet = new ProtoWallet(new PrivateKey(1))
57
+ verifierWallet = new ProtoWallet(new PrivateKey(1)) as unknown as WalletInterface
57
58
  message = {
59
+ version: '1.0',
60
+ messageType: 'certificateResponse',
58
61
  identityKey: 'valid_subject',
59
62
  certificates: [
60
63
  {
@@ -65,7 +68,7 @@ describe('validateCertificates', () => {
65
68
  revocationOutpoint: 'outpoint',
66
69
  fields: { field1: 'encryptedData1' },
67
70
  decryptedFields: {}
68
- }
71
+ } as any
69
72
  ]
70
73
  }
71
74
  })
@@ -76,9 +79,9 @@ describe('validateCertificates', () => {
76
79
  ).resolves.not.toThrow()
77
80
 
78
81
  expect(VerifiableCertificate).toHaveBeenCalledTimes(
79
- message.certificates.length
82
+ message.certificates!.length
80
83
  )
81
- expect(mockVerify).toHaveBeenCalledTimes(message.certificates.length)
84
+ expect(mockVerify).toHaveBeenCalledTimes(message.certificates!.length)
82
85
  expect(mockDecryptFields).toHaveBeenCalledWith(verifierWallet, undefined, undefined, undefined)
83
86
  })
84
87
 
@@ -147,9 +150,9 @@ describe('validateCertificates', () => {
147
150
  revocationOutpoint: 'outpoint',
148
151
  fields: { field1: 'encryptedData1' },
149
152
  decryptedFields: {}
150
- }
153
+ } as any
151
154
 
152
- message.certificates.push(anotherCertificate)
155
+ message.certificates!.push(anotherCertificate)
153
156
 
154
157
  await expect(
155
158
  validateCertificates(verifierWallet, message)
@@ -6,13 +6,11 @@ import * as Utils from '../../primitives/utils.js'
6
6
  import {
7
7
  WalletInterface,
8
8
  ListOutputsResult,
9
- WalletDecryptResult,
10
9
  WalletEncryptResult,
11
10
  CreateActionResult,
12
11
  SignActionResult
13
12
  } from '../../wallet/Wallet.interfaces.js'
14
13
  import Transaction from '../../transaction/Transaction.js'
15
- import { Beef } from '../../transaction/Beef.js'
16
14
  import { mock } from 'node:test'
17
15
 
18
16
  // --- Constants for Mock Values ---
@@ -303,7 +301,7 @@ describe('localKVStore', () => {
303
301
  const existingOutput = { outpoint: existingOutpoint, txid: 'oldTxId', vout: 0, lockingScript: 'oldScriptHex' } // Added script
304
302
  const mockBEEF = [1, 2, 3, 4, 5, 6]
305
303
  const signableRef = 'signableTxRef123'
306
- const signableTx = []
304
+ const signableTx: any[] = []
307
305
  const updatedTxId = 'updatedTxId'
308
306
 
309
307
  const valueArray = Array.from(testRawValueBuffer)
@@ -397,7 +395,7 @@ describe('localKVStore', () => {
397
395
  const existingOutput2 = { outpoint: existingOutpoint2, txid: 'oldTxId2', vout: 1, lockingScript: 's2' }
398
396
  const mockBEEF = [1, 2, 3, 4, 5, 6]
399
397
  const signableRef = 'signableTxRefMulti'
400
- const signableTx = []
398
+ const signableTx: any[] = []
401
399
  const updatedTxId = 'updatedTxIdMulti'
402
400
  const mockTxObject = {} // Dummy TX object
403
401
 
@@ -564,7 +562,7 @@ describe('localKVStore', () => {
564
562
  const existingOutput2 = { outpoint: existingOutpoint2, txid: 'removeTxId2', vout: 1, lockingScript: 's2' }
565
563
  const mockBEEF = Buffer.from('mockBEEFRemove')
566
564
  const signableRef = 'signableTxRefRemove'
567
- const signableTx = []
565
+ const signableTx: any[] = []
568
566
  const removalTxId = 'removalTxId'
569
567
  const mockTxObject = {}
570
568
 
@@ -628,7 +626,7 @@ describe('localKVStore', () => {
628
626
  const existingOutput1 = { outpoint: existingOutpoint1, txid: 'failRemoveTxId1', vout: 0, lockingScript: 's1' }
629
627
  const mockBEEF = Buffer.from('mockBEEFFailRemove')
630
628
  const signableRef = 'signableTxRefFailRemove'
631
- const signableTx = []
629
+ const signableTx: any[] = []
632
630
  const mockTxObject = {}
633
631
 
634
632
  mockWallet.listOutputs.mockResolvedValue({ outputs: [existingOutput1], totalOutputs: 1, BEEF: mockBEEF } as any)
@@ -67,12 +67,14 @@ export class HostReputationTracker {
67
67
  const now = Date.now()
68
68
  entry.totalFailures += 1
69
69
  entry.consecutiveFailures += 1
70
- const msg =
71
- typeof reason === 'string'
72
- ? reason
73
- : reason instanceof Error
74
- ? reason.message
75
- : undefined
70
+ let msg: string | undefined
71
+ if (typeof reason === 'string') {
72
+ msg = reason
73
+ } else if (reason instanceof Error) {
74
+ msg = reason.message
75
+ } else {
76
+ msg = undefined
77
+ }
76
78
  const immediate =
77
79
  typeof msg === 'string' &&
78
80
  (msg.includes('ERR_NAME_NOT_RESOLVED') ||
@@ -93,12 +95,13 @@ export class HostReputationTracker {
93
95
  entry.backoffUntil = now + backoffDuration
94
96
  }
95
97
  entry.lastUpdatedAt = now
96
- entry.lastError =
97
- typeof reason === 'string'
98
- ? reason
99
- : reason instanceof Error
100
- ? reason.message
101
- : undefined
98
+ if (typeof reason === 'string') {
99
+ entry.lastError = reason
100
+ } else if (reason instanceof Error) {
101
+ entry.lastError = reason.message
102
+ } else {
103
+ entry.lastError = undefined
104
+ }
102
105
  this.saveToStorage()
103
106
  }
104
107
 
@@ -133,13 +136,13 @@ export class HostReputationTracker {
133
136
 
134
137
  snapshot (host: string): HostReputationEntry | undefined {
135
138
  const entry = this.stats.get(host)
136
- return entry != null ? { ...entry } : undefined
139
+ return entry == null ? undefined : { ...entry }
137
140
  }
138
141
 
139
142
  private getStorage (): any {
140
143
  try {
141
144
  const g: any = typeof globalThis === 'object' ? globalThis : undefined
142
- if (g == null || g.localStorage == null) return undefined
145
+ if (g?.localStorage == null) return undefined
143
146
  return g.localStorage
144
147
  } catch {
145
148
  return undefined
@@ -55,7 +55,7 @@ export class KeyShares {
55
55
  const [x, y, t, i] = shareParts
56
56
  if (t === undefined) throw new Error('Threshold not found in share ' + idx.toString())
57
57
  if (i === undefined) throw new Error('Integrity not found in share ' + idx.toString())
58
- const tInt = parseInt(t)
58
+ const tInt = Number.parseInt(t, 10)
59
59
  if (idx !== 0 && threshold !== tInt) { throw new Error('Threshold mismatch in share ' + idx.toString()) }
60
60
  if (idx !== 0 && integrity !== i) { throw new Error('Integrity mismatch in share ' + idx.toString()) }
61
61
  threshold = tInt
@@ -399,7 +399,7 @@ export default class PrivateKey extends BigNumber {
399
399
  let sharedSecret: Point
400
400
  if (typeof retrieveCachedSharedSecret === 'function') {
401
401
  const retrieved = retrieveCachedSharedSecret(this, publicKey)
402
- if (typeof retrieved !== 'undefined') {
402
+ if (retrieved !== undefined) {
403
403
  sharedSecret = retrieved
404
404
  } else {
405
405
  sharedSecret = this.deriveSharedSecret(publicKey)
@@ -429,7 +429,7 @@ export default class PrivateKey extends BigNumber {
429
429
  * const shares = key.toKeyShares(2, 5)
430
430
  */
431
431
  toKeyShares (threshold: number, totalShares: number): KeyShares {
432
- if (typeof threshold !== 'number' || typeof totalShares !== 'number') { throw new Error('threshold and totalShares must be numbers') }
432
+ if (typeof threshold !== 'number' || typeof totalShares !== 'number') { throw new TypeError('threshold and totalShares must be numbers') }
433
433
  if (threshold < 2) throw new Error('threshold must be at least 2')
434
434
  if (totalShares < 2) throw new Error('totalShares must be at least 2')
435
435
  if (threshold > totalShares) { throw new Error('threshold should be less than or equal to totalShares') }
@@ -266,7 +266,11 @@ export default class Spend {
266
266
  if (this.stack.length === 0) {
267
267
  this.scriptEvaluationError('Attempted to pop from an empty stack.')
268
268
  }
269
- const item = this.stack.pop() as number[]
269
+ const item = this.stack.pop()
270
+ if (item === undefined) {
271
+ this.scriptEvaluationError('Attempted to pop from an empty stack.')
272
+ return [] // unreachable; scriptEvaluationError always throws
273
+ }
270
274
  this.stackMem -= item.length
271
275
  return item
272
276
  }
@@ -290,7 +294,11 @@ export default class Spend {
290
294
  if (this.altStack.length === 0) {
291
295
  this.scriptEvaluationError('Attempted to pop from an empty alt stack.')
292
296
  }
293
- const item = this.altStack.pop() as number[]
297
+ const item = this.altStack.pop()
298
+ if (item === undefined) {
299
+ this.scriptEvaluationError('Attempted to pop from an empty alt stack.')
300
+ return [] // unreachable; scriptEvaluationError always throws
301
+ }
294
302
  this.altStackMem -= item.length
295
303
  return item
296
304
  }
@@ -308,7 +316,7 @@ export default class Spend {
308
316
  this.scriptEvaluationError('The signature must have a low S value.')
309
317
  return false
310
318
  }
311
- } catch (e) {
319
+ } catch {
312
320
  this.scriptEvaluationError('The signature format is invalid.')
313
321
  return false
314
322
  }
@@ -340,7 +348,7 @@ export default class Spend {
340
348
  }
341
349
  try {
342
350
  PublicKey.fromDER(buf as number[]) // This can throw for stricter DER rules
343
- } catch (e) {
351
+ } catch {
344
352
  this.scriptEvaluationError('The public key is in an unknown format.')
345
353
  return false
346
354
  }
@@ -395,7 +403,7 @@ export default class Spend {
395
403
  const operation = currentScript.chunks[this.programCounter]
396
404
 
397
405
  const currentOpcode = operation.op
398
- if (typeof currentOpcode === 'undefined') {
406
+ if (currentOpcode === undefined) {
399
407
  this.scriptEvaluationError(`Missing opcode in ${this.context} at pc=${this.programCounter}.`) // Error thrown
400
408
  }
401
409
  if (Array.isArray(operation.data) && operation.data.length > maxScriptElementSize) {
@@ -547,7 +555,7 @@ export default class Spend {
547
555
  break
548
556
  case OP.OP_ELSE:
549
557
  if (this.ifStack.length === 0) this.scriptEvaluationError('OP_ELSE requires a preceeding OP_IF.')
550
- this.ifStack[this.ifStack.length - 1] = !this.ifStack[this.ifStack.length - 1]
558
+ this.ifStack[this.ifStack.length - 1] = !(this.ifStack[this.ifStack.length - 1])
551
559
  break
552
560
  case OP.OP_ENDIF:
553
561
  if (this.ifStack.length === 0) this.scriptEvaluationError('OP_ENDIF requires a preceeding OP_IF.')
@@ -682,7 +690,7 @@ export default class Spend {
682
690
  // stack is [... rest, x1, x2]
683
691
  // We want [... rest, x2_copy, x1, x2]
684
692
  this.ensureStackMem(buf1.length)
685
- this.stack.splice(this.stack.length - 2, 0, buf1.slice()) // Insert copy of x2 before x1
693
+ this.stack.splice(-2, 0, buf1.slice()) // Insert copy of x2 before x1
686
694
  this.stackMem += buf1.length // Account for the new copy
687
695
  break
688
696
  case OP.OP_SIZE:
@@ -769,7 +777,7 @@ export default class Spend {
769
777
  case OP.OP_NEGATE: bn = bn.neg(); break
770
778
  case OP.OP_ABS: if (bn.isNeg()) bn = bn.neg(); break
771
779
  case OP.OP_NOT: bn = new BigNumber(bn.cmpn(0) === 0 ? 1 : 0); break
772
- case OP.OP_0NOTEQUAL: bn = new BigNumber(bn.cmpn(0) !== 0 ? 1 : 0); break
780
+ case OP.OP_0NOTEQUAL: bn = new BigNumber(bn.cmpn(0) === 0 ? 0 : 1); break
773
781
  }
774
782
  this.pushStack(bn.toScriptNum())
775
783
  break
@@ -812,7 +820,7 @@ export default class Spend {
812
820
  case OP.OP_BOOLOR: resultBnArithmetic = new BigNumber((bn1.cmpn(0) !== 0 || bn2.cmpn(0) !== 0) ? 1 : 0); break
813
821
  case OP.OP_NUMEQUAL: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) === 0 ? 1 : 0); break
814
822
  case OP.OP_NUMEQUALVERIFY: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) === 0 ? 1 : 0); break
815
- case OP.OP_NUMNOTEQUAL: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) !== 0 ? 1 : 0); break
823
+ case OP.OP_NUMNOTEQUAL: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) === 0 ? 0 : 1); break
816
824
  case OP.OP_LESSTHAN: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) < 0 ? 1 : 0); break
817
825
  case OP.OP_GREATERTHAN: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) > 0 ? 1 : 0); break
818
826
  case OP.OP_LESSTHANOREQUAL: resultBnArithmetic = new BigNumber(bn1.cmp(bn2) <= 0 ? 1 : 0); break
@@ -875,7 +883,7 @@ export default class Spend {
875
883
 
876
884
  pubkey = PublicKey.fromDER(bufPubkey)
877
885
  fSuccess = this.verifySignature(sig, pubkey, subscript)
878
- } catch (e) {
886
+ } catch {
879
887
  fSuccess = false
880
888
  }
881
889
  }
@@ -949,7 +957,7 @@ export default class Spend {
949
957
  sig = TransactionSignature.fromChecksigFormat(bufSig)
950
958
  pubkey = PublicKey.fromDER(bufPubkey)
951
959
  fOk = this.verifySignature(sig, pubkey, subscript)
952
- } catch (e) {
960
+ } catch {
953
961
  fOk = false
954
962
  }
955
963
  }
@@ -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
  }