@bsv/wallet-toolbox 1.7.19 → 1.7.20

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 (31) hide show
  1. package/docs/client.md +26 -2
  2. package/docs/services.md +13 -1
  3. package/docs/storage.md +13 -1
  4. package/docs/wallet.md +26 -2
  5. package/out/src/WalletPermissionsManager.d.ts +6 -0
  6. package/out/src/WalletPermissionsManager.d.ts.map +1 -1
  7. package/out/src/WalletPermissionsManager.js +241 -74
  8. package/out/src/WalletPermissionsManager.js.map +1 -1
  9. package/out/src/services/chaintracker/chaintracks/Ingest/BulkIngestorWhatsOnChainCdn.d.ts +1 -1
  10. package/out/src/services/chaintracker/chaintracks/__tests/Chaintracks.test.js +3 -1
  11. package/out/src/services/chaintracker/chaintracks/__tests/Chaintracks.test.js.map +1 -1
  12. package/out/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.d.ts.map +1 -1
  13. package/out/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.js +12 -0
  14. package/out/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.js.map +1 -1
  15. package/out/src/storage/StorageProvider.d.ts +16 -2
  16. package/out/src/storage/StorageProvider.d.ts.map +1 -1
  17. package/out/src/storage/StorageProvider.js +33 -4
  18. package/out/src/storage/StorageProvider.js.map +1 -1
  19. package/out/src/storage/methods/__test/offsetKey.test.js +266 -100
  20. package/out/src/storage/methods/__test/offsetKey.test.js.map +1 -1
  21. package/out/src/storage/methods/getBeefForTransaction.js +1 -1
  22. package/out/src/storage/methods/getBeefForTransaction.js.map +1 -1
  23. package/out/tsconfig.all.tsbuildinfo +1 -1
  24. package/package.json +1 -1
  25. package/src/WalletPermissionsManager.ts +302 -71
  26. package/src/services/chaintracker/chaintracks/Ingest/BulkIngestorWhatsOnChainCdn.ts +1 -1
  27. package/src/services/chaintracker/chaintracks/__tests/Chaintracks.test.ts +2 -1
  28. package/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.ts +12 -0
  29. package/src/storage/StorageProvider.ts +38 -5
  30. package/src/storage/methods/__test/offsetKey.test.ts +299 -103
  31. package/src/storage/methods/getBeefForTransaction.ts +3 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/wallet-toolbox",
3
- "version": "1.7.19",
3
+ "version": "1.7.20",
4
4
  "description": "BRC100 conforming wallet, wallet storage and wallet signer components",
5
5
  "main": "./out/src/index.js",
6
6
  "types": "./out/src/index.d.ts",
@@ -831,70 +831,80 @@ export class WalletPermissionsManager implements WalletInterface {
831
831
 
832
832
  const expiry = params.expiry || 0 // default: never expires
833
833
 
834
+ const toCreate: Array<{ request: PermissionRequest; expiry: number; amount?: number }> = []
835
+ const toRenew: Array<{ oldToken: PermissionToken; request: PermissionRequest; expiry: number; amount?: number }> =
836
+ []
837
+
834
838
  if (params.granted.spendingAuthorization) {
835
- await this.createPermissionOnChain(
836
- {
839
+ toCreate.push({
840
+ request: {
837
841
  type: 'spending',
838
842
  originator,
839
843
  spending: { satoshis: params.granted.spendingAuthorization.amount },
840
844
  reason: params.granted.spendingAuthorization.description
841
845
  },
842
- 0, // No expiry for spending tokens
843
- params.granted.spendingAuthorization.amount
844
- )
846
+ expiry: 0,
847
+ amount: params.granted.spendingAuthorization.amount
848
+ })
845
849
  }
846
- for (const p of params.granted.protocolPermissions || []) {
850
+
851
+ const grantedProtocols = params.granted.protocolPermissions || []
852
+ const protocolTokens = await this.mapWithConcurrency(grantedProtocols, 8, async p => {
847
853
  const token = await this.findProtocolToken(
848
854
  originator,
849
- false, // No privileged protocols allowed in groups for added security.
855
+ false,
850
856
  p.protocolID,
851
857
  p.counterparty || 'self',
852
858
  true,
853
859
  originLookupValues
854
860
  )
861
+ return { p, token }
862
+ })
863
+
864
+ for (const { p, token } of protocolTokens) {
865
+ const request: PermissionRequest = {
866
+ type: 'protocol',
867
+ originator,
868
+ privileged: false,
869
+ protocolID: p.protocolID,
870
+ counterparty: p.counterparty || 'self',
871
+ reason: p.description
872
+ }
855
873
  if (token) {
856
- const request: PermissionRequest = {
857
- type: 'protocol',
858
- originator,
859
- privileged: false, // No privileged protocols allowed in groups for added security.
860
- protocolID: p.protocolID,
861
- counterparty: p.counterparty || 'self',
862
- reason: p.description
863
- }
864
- await this.renewPermissionOnChain(token, request, expiry)
865
- this.markRecentGrant(request)
874
+ toRenew.push({ oldToken: token, request, expiry })
866
875
  } else {
867
- const request: PermissionRequest = {
868
- type: 'protocol',
869
- originator,
870
- privileged: false, // No privileged protocols allowed in groups for added security.
871
- protocolID: p.protocolID,
872
- counterparty: p.counterparty || 'self',
873
- reason: p.description
874
- }
875
- await this.createPermissionOnChain(request, expiry)
876
- this.markRecentGrant(request)
876
+ toCreate.push({ request, expiry })
877
877
  }
878
878
  }
879
+
879
880
  for (const b of params.granted.basketAccess || []) {
880
- const request: PermissionRequest = { type: 'basket', originator, basket: b.basket, reason: b.description }
881
- await this.createPermissionOnChain(request, expiry)
882
- this.markRecentGrant(request)
881
+ toCreate.push({
882
+ request: { type: 'basket', originator, basket: b.basket, reason: b.description },
883
+ expiry
884
+ })
883
885
  }
886
+
884
887
  for (const c of params.granted.certificateAccess || []) {
885
- const request: PermissionRequest = {
886
- type: 'certificate',
887
- originator,
888
- privileged: false, // No certificates on the privileged identity are allowed as part of groups.
889
- certificate: {
890
- verifier: c.verifierPublicKey,
891
- certType: c.type,
892
- fields: c.fields
888
+ toCreate.push({
889
+ request: {
890
+ type: 'certificate',
891
+ originator,
892
+ privileged: false,
893
+ certificate: {
894
+ verifier: c.verifierPublicKey,
895
+ certType: c.type,
896
+ fields: c.fields
897
+ },
898
+ reason: c.description
893
899
  },
894
- reason: c.description
895
- }
896
- await this.createPermissionOnChain(request, expiry)
897
- this.markRecentGrant(request)
900
+ expiry
901
+ })
902
+ }
903
+
904
+ const created = await this.createPermissionTokensBestEffort(toCreate)
905
+ const renewed = await this.renewPermissionTokensBestEffort(toRenew)
906
+ for (const req of [...created, ...renewed]) {
907
+ this.markRecentGrant(req)
898
908
  }
899
909
 
900
910
  // Resolve all pending promises for this request
@@ -958,7 +968,12 @@ export class WalletPermissionsManager implements WalletInterface {
958
968
 
959
969
  const expiry = params.expiry || 0
960
970
 
961
- for (const p of params.granted.protocols || []) {
971
+ const toCreate: Array<{ request: PermissionRequest; expiry: number; amount?: number }> = []
972
+ const toRenew: Array<{ oldToken: PermissionToken; request: PermissionRequest; expiry: number; amount?: number }> =
973
+ []
974
+
975
+ const grantedProtocols = params.granted.protocols || []
976
+ const protocolTokens = await this.mapWithConcurrency(grantedProtocols, 8, async p => {
962
977
  const token = await this.findProtocolToken(
963
978
  originator,
964
979
  false,
@@ -967,32 +982,30 @@ export class WalletPermissionsManager implements WalletInterface {
967
982
  true,
968
983
  originLookupValues
969
984
  )
985
+ return { p, token }
986
+ })
987
+
988
+ for (const { p, token } of protocolTokens) {
989
+ const request: PermissionRequest = {
990
+ type: 'protocol',
991
+ originator,
992
+ privileged: false,
993
+ protocolID: p.protocolID,
994
+ counterparty,
995
+ reason: p.description
996
+ }
970
997
  if (token) {
971
- const request: PermissionRequest = {
972
- type: 'protocol',
973
- originator,
974
- privileged: false,
975
- protocolID: p.protocolID,
976
- counterparty,
977
- reason: p.description
978
- }
979
- await this.renewPermissionOnChain(token, request, expiry)
980
- this.markRecentGrant(request)
998
+ toRenew.push({ oldToken: token, request, expiry })
981
999
  } else {
982
- const request: PermissionRequest = {
983
- type: 'protocol',
984
- originator,
985
- privileged: false,
986
- protocolID: p.protocolID,
987
- counterparty,
988
- reason: p.description
989
- }
990
- await this.createPermissionOnChain(request, expiry)
991
- this.markRecentGrant(request)
1000
+ toCreate.push({ request, expiry })
992
1001
  }
993
1002
  }
994
1003
 
995
- this.markPactEstablished(originator, counterparty)
1004
+ const created = await this.createPermissionTokensBestEffort(toCreate)
1005
+ const renewed = await this.renewPermissionTokensBestEffort(toRenew)
1006
+ for (const req of [...created, ...renewed]) {
1007
+ this.markRecentGrant(req)
1008
+ }
996
1009
 
997
1010
  for (const p of matching.pending) {
998
1011
  p.resolve(true)
@@ -2574,13 +2587,178 @@ export class WalletPermissionsManager implements WalletInterface {
2574
2587
  }
2575
2588
  ],
2576
2589
  options: {
2577
- acceptDelayedBroadcast: false
2590
+ acceptDelayedBroadcast: true
2578
2591
  }
2579
2592
  },
2580
2593
  this.adminOriginator
2581
2594
  )
2582
2595
  }
2583
2596
 
2597
+ private async mapWithConcurrency<T, R>(items: T[], concurrency: number, fn: (item: T) => Promise<R>): Promise<R[]> {
2598
+ if (!items.length) return []
2599
+ const results: R[] = new Array(items.length)
2600
+ let i = 0
2601
+ const worker = async () => {
2602
+ while (true) {
2603
+ const idx = i++
2604
+ if (idx >= items.length) return
2605
+ results[idx] = await fn(items[idx])
2606
+ }
2607
+ }
2608
+ await Promise.all(Array.from({ length: Math.min(concurrency, items.length) }, () => worker()))
2609
+ return results
2610
+ }
2611
+
2612
+ private async runBestEffortBatches<T, R>(
2613
+ items: T[],
2614
+ chunkSize: number,
2615
+ runChunk: (chunk: T[]) => Promise<R[]>
2616
+ ): Promise<R[]> {
2617
+ if (!items.length) return []
2618
+ const out: R[] = []
2619
+ for (let i = 0; i < items.length; i += chunkSize) {
2620
+ const chunk = items.slice(i, i + chunkSize)
2621
+ out.push(...(await this.runBestEffortChunk(chunk, runChunk)))
2622
+ }
2623
+ return out
2624
+ }
2625
+
2626
+ private async runBestEffortChunk<T, R>(chunk: T[], runChunk: (chunk: T[]) => Promise<R[]>): Promise<R[]> {
2627
+ try {
2628
+ return await runChunk(chunk)
2629
+ } catch (e) {
2630
+ if (chunk.length <= 1) {
2631
+ console.error('Permission batch failed:', e)
2632
+ return []
2633
+ }
2634
+ const mid = Math.ceil(chunk.length / 2)
2635
+ const left = await this.runBestEffortChunk(chunk.slice(0, mid), runChunk)
2636
+ const right = await this.runBestEffortChunk(chunk.slice(mid), runChunk)
2637
+ return [...left, ...right]
2638
+ }
2639
+ }
2640
+
2641
+ private async buildPermissionOutput(
2642
+ r: PermissionRequest,
2643
+ expiry: number,
2644
+ amount?: number
2645
+ ): Promise<{
2646
+ output: { lockingScript: string; satoshis: number; outputDescription: string; basket: string; tags: string[] }
2647
+ request: PermissionRequest
2648
+ }> {
2649
+ const normalizedOriginator = this.normalizeOriginator(r.originator) || r.originator
2650
+ r.originator = normalizedOriginator
2651
+ const basketName = BASKET_MAP[r.type]
2652
+ if (!basketName) {
2653
+ throw new Error(`Unsupported permission type: ${r.type}`)
2654
+ }
2655
+ const fields: number[][] = await this.buildPushdropFields(r, expiry, amount)
2656
+ const script = await new PushDrop(this.underlying).lock(
2657
+ fields,
2658
+ WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
2659
+ '1',
2660
+ 'self',
2661
+ true,
2662
+ true
2663
+ )
2664
+ const tags = this.buildTagsForRequest(r)
2665
+ return {
2666
+ request: r,
2667
+ output: {
2668
+ lockingScript: script.toHex(),
2669
+ satoshis: 1,
2670
+ outputDescription: `${r.type} permission token`,
2671
+ basket: basketName,
2672
+ tags
2673
+ }
2674
+ }
2675
+ }
2676
+
2677
+ private async createPermissionTokensBestEffort(
2678
+ items: Array<{ request: PermissionRequest; expiry: number; amount?: number }>
2679
+ ): Promise<PermissionRequest[]> {
2680
+ const CHUNK = 25
2681
+ return this.runBestEffortBatches(items, CHUNK, async chunk => {
2682
+ const built = await this.mapWithConcurrency(chunk, 8, c =>
2683
+ this.buildPermissionOutput(c.request, c.expiry, c.amount)
2684
+ )
2685
+ await this.createAction(
2686
+ {
2687
+ description: `Grant ${built.length} permissions`,
2688
+ outputs: built.map(b => b.output),
2689
+ options: { acceptDelayedBroadcast: true }
2690
+ },
2691
+ this.adminOriginator
2692
+ )
2693
+ return built.map(b => b.request)
2694
+ })
2695
+ }
2696
+
2697
+ private async renewPermissionTokensBestEffort(
2698
+ items: Array<{ oldToken: PermissionToken; request: PermissionRequest; expiry: number; amount?: number }>
2699
+ ): Promise<PermissionRequest[]> {
2700
+ const CHUNK = 15
2701
+ return this.runBestEffortBatches(items, CHUNK, async chunk => {
2702
+ const built = await this.mapWithConcurrency(chunk, 8, c =>
2703
+ this.buildPermissionOutput(c.request, c.expiry, c.amount)
2704
+ )
2705
+
2706
+ const inputBeef = new Beef()
2707
+ for (const c of chunk) {
2708
+ inputBeef.mergeBeef(Beef.fromBinary(c.oldToken.tx))
2709
+ }
2710
+
2711
+ const { signableTransaction } = await this.createAction(
2712
+ {
2713
+ description: `Renew ${chunk.length} permissions`,
2714
+ inputBEEF: inputBeef.toBinary(),
2715
+ inputs: chunk.map((c, i) => ({
2716
+ outpoint: `${c.oldToken.txid}.${c.oldToken.outputIndex}`,
2717
+ unlockingScriptLength: 73,
2718
+ inputDescription: `Consume old permission token #${i + 1}`
2719
+ })),
2720
+ outputs: built.map(b => b.output),
2721
+ options: {
2722
+ acceptDelayedBroadcast: true,
2723
+ randomizeOutputs: false,
2724
+ signAndProcess: false
2725
+ }
2726
+ },
2727
+ this.adminOriginator
2728
+ )
2729
+
2730
+ if (!signableTransaction?.reference || !signableTransaction.tx) {
2731
+ throw new Error('Failed to create signable transaction')
2732
+ }
2733
+
2734
+ const partialTx = Transaction.fromAtomicBEEF(signableTransaction.tx)
2735
+ const pushdrop = new PushDrop(this.underlying)
2736
+ const spends: Record<number, { unlockingScript: string }> = {}
2737
+
2738
+ for (let i = 0; i < chunk.length; i++) {
2739
+ const token = chunk[i].oldToken
2740
+ const unlocker = pushdrop.unlock(
2741
+ WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
2742
+ '1',
2743
+ 'self',
2744
+ 'all',
2745
+ false,
2746
+ 1,
2747
+ LockingScript.fromHex(token.outputScript)
2748
+ )
2749
+ const unlockingScript = await unlocker.sign(partialTx, i)
2750
+ spends[i] = { unlockingScript: unlockingScript.toHex() }
2751
+ }
2752
+
2753
+ const { txid } = await this.underlying.signAction({
2754
+ reference: signableTransaction.reference,
2755
+ spends
2756
+ })
2757
+ if (!txid) throw new Error('Failed to finalize renewal transaction')
2758
+ return built.map(b => b.request)
2759
+ })
2760
+ }
2761
+
2584
2762
  private async coalescePermissionTokens(
2585
2763
  oldTokens: PermissionToken[],
2586
2764
  newScript: LockingScript,
@@ -2618,7 +2796,7 @@ export class WalletPermissionsManager implements WalletInterface {
2618
2796
  }
2619
2797
  ],
2620
2798
  options: {
2621
- acceptDelayedBroadcast: false,
2799
+ acceptDelayedBroadcast: true,
2622
2800
  randomizeOutputs: false,
2623
2801
  signAndProcess: false
2624
2802
  }
@@ -2730,7 +2908,7 @@ export class WalletPermissionsManager implements WalletInterface {
2730
2908
  }
2731
2909
  ],
2732
2910
  options: {
2733
- acceptDelayedBroadcast: false
2911
+ acceptDelayedBroadcast: true
2734
2912
  }
2735
2913
  },
2736
2914
  this.adminOriginator
@@ -2898,7 +3076,7 @@ export class WalletPermissionsManager implements WalletInterface {
2898
3076
  tags,
2899
3077
  tagQueryMode: 'all',
2900
3078
  include: 'entire transactions',
2901
- limit: 100
3079
+ limit: 10000
2902
3080
  },
2903
3081
  this.adminOriginator
2904
3082
  )
@@ -3245,12 +3423,65 @@ export class WalletPermissionsManager implements WalletInterface {
3245
3423
  }
3246
3424
  ],
3247
3425
  options: {
3248
- acceptDelayedBroadcast: false
3426
+ acceptDelayedBroadcast: true
3249
3427
  }
3250
3428
  },
3251
3429
  this.adminOriginator
3252
3430
  )
3253
3431
  const tx = Transaction.fromBEEF(signableTransaction!.tx)
3432
+
3433
+ const normalizeTxid = (txid?: string) => (txid ?? '').toLowerCase()
3434
+ const reverseHexTxid = (txid: string) => {
3435
+ const hex = normalizeTxid(txid)
3436
+ if (!/^[0-9a-f]{64}$/.test(hex)) return hex
3437
+ const bytes = hex.match(/../g)
3438
+ return bytes ? bytes.reverse().join('') : hex
3439
+ }
3440
+ const matchesOutpointString = (outpoint: string) => {
3441
+ const dot = outpoint.lastIndexOf('.')
3442
+ const colon = outpoint.lastIndexOf(':')
3443
+ const sep = dot > colon ? dot : colon
3444
+ if (sep === -1) return false
3445
+ const txidPart = outpoint.slice(0, sep)
3446
+ const indexPart = outpoint.slice(sep + 1)
3447
+ const vout = Number(indexPart)
3448
+ if (!Number.isFinite(vout)) return false
3449
+ return normalizeTxid(txidPart) === normalizeTxid(oldToken.txid) && vout === oldToken.outputIndex
3450
+ }
3451
+
3452
+ let permInputIndex = tx.inputs.findIndex((input: any) => {
3453
+ const txidCandidate: unknown =
3454
+ input?.sourceTXID ??
3455
+ input?.sourceTxid ??
3456
+ input?.sourceTxId ??
3457
+ input?.prevTxId ??
3458
+ input?.prevTxid ??
3459
+ input?.prevTXID ??
3460
+ input?.txid ??
3461
+ input?.txID
3462
+
3463
+ const voutCandidate: unknown =
3464
+ input?.sourceOutputIndex ?? input?.sourceOutput ?? input?.outputIndex ?? input?.vout ?? input?.prevOutIndex
3465
+
3466
+ if (typeof txidCandidate === 'string' && typeof voutCandidate === 'number') {
3467
+ const cand = normalizeTxid(txidCandidate)
3468
+ const target = normalizeTxid(oldToken.txid)
3469
+ if (cand === target && voutCandidate === oldToken.outputIndex) return true
3470
+ if (cand === reverseHexTxid(oldToken.txid) && voutCandidate === oldToken.outputIndex) return true
3471
+ }
3472
+
3473
+ const outpointCandidate: unknown = input?.outpoint ?? input?.sourceOutpoint ?? input?.prevOutpoint
3474
+ if (typeof outpointCandidate === 'string' && matchesOutpointString(outpointCandidate)) return true
3475
+
3476
+ return false
3477
+ })
3478
+
3479
+ if (permInputIndex === -1 && tx.inputs.length === 1) {
3480
+ permInputIndex = 0
3481
+ }
3482
+ if (permInputIndex === -1) {
3483
+ throw new Error('Unable to locate permission token input for revocation.')
3484
+ }
3254
3485
  const unlocker = new PushDrop(this.underlying).unlock(
3255
3486
  WalletPermissionsManager.PERM_TOKEN_ENCRYPTION_PROTOCOL,
3256
3487
  '1',
@@ -3260,11 +3491,11 @@ export class WalletPermissionsManager implements WalletInterface {
3260
3491
  1,
3261
3492
  LockingScript.fromHex(oldToken.outputScript)
3262
3493
  )
3263
- const unlockingScript = await unlocker.sign(tx, 0)
3494
+ const unlockingScript = await unlocker.sign(tx, permInputIndex)
3264
3495
  await this.underlying.signAction({
3265
3496
  reference: signableTransaction!.reference,
3266
3497
  spends: {
3267
- 0: {
3498
+ [permInputIndex]: {
3268
3499
  unlockingScript: unlockingScript.toHex()
3269
3500
  }
3270
3501
  }
@@ -11,7 +11,7 @@ import { WhatsOnChainServices, WhatsOnChainServicesOptions } from './WhatsOnChai
11
11
 
12
12
  export interface BulkIngestorWhatsOnChainOptions extends BulkIngestorBaseOptions, WhatsOnChainServicesOptions {
13
13
  /**
14
- * Maximum msces of "normal" pause with no new data arriving.
14
+ * Maximum msecs of "normal" pause with no new data arriving.
15
15
  */
16
16
  idleWait: number | undefined
17
17
  /**
@@ -35,7 +35,8 @@ describe('Chaintracks tests', () => {
35
35
  await NoDbBody('test')
36
36
  })
37
37
 
38
- test.skip('3 NoDb export mainnet', async () => {
38
+ test('3 NoDb export mainnet', async () => {
39
+ if (_tu.noEnv('main')) return
39
40
  await NoDbBody('main', true)
40
41
  })
41
42
 
@@ -428,5 +428,17 @@ export const validBulkHeaderFiles: BulkHeaderFileInfo[] = [
428
428
  prevHash: '00000000000000000e7dcc27c06ee353bd37260b2e7e664314c204f0324a5087',
429
429
  sourceUrl: 'https://cdn.projectbabbage.com/blockheaders',
430
430
  validated: true
431
+ },
432
+ {
433
+ chain: 'main',
434
+ count: 31772,
435
+ fileHash: 'NuVsRUrI5QnjILbYy4LS3A/Udl6PH/m8Y9uVguEsekM=',
436
+ fileName: 'mainNet_9.headers',
437
+ firstHeight: 900000,
438
+ lastChainWork: '0000000000000000000000000000000000000000016ab16bb9b31430588788d3',
439
+ lastHash: '0000000000000000024a2f1caef4b0ffdc1a036b09f9ed7f48b538f619f32ef2',
440
+ prevChainWork: '000000000000000000000000000000000000000001664db1f2d50327928007e0',
441
+ prevHash: '00000000000000000e7dcc27c06ee353bd37260b2e7e664314c204f0324a5087',
442
+ sourceUrl: 'https://cdn.projectbabbage.com/blockheaders'
431
443
  }
432
444
  ]
@@ -12,7 +12,8 @@ import {
12
12
  RelinquishOutputArgs,
13
13
  AbortActionArgs,
14
14
  Validation,
15
- WalletLoggerInterface
15
+ WalletLoggerInterface,
16
+ ChainTracker
16
17
  } from '@bsv/sdk'
17
18
  import { getBeefForTransaction } from './methods/getBeefForTransaction'
18
19
  import { GetReqsAndBeefDetail, GetReqsAndBeefResult, processAction } from './methods/processAction'
@@ -57,6 +58,7 @@ import { TableMonitorEvent } from '../../src/storage/schema/tables/TableMonitorE
57
58
  import { TableCertificateX } from './schema/tables/TableCertificate'
58
59
  import {
59
60
  WERR_INTERNAL,
61
+ WERR_INVALID_MERKLE_ROOT,
60
62
  WERR_INVALID_OPERATION,
61
63
  WERR_INVALID_PARAMETER,
62
64
  WERR_MISSING_PARAMETER,
@@ -438,6 +440,20 @@ export abstract class StorageProvider extends StorageReaderWriter implements Wal
438
440
  return proven != undefined || rawTx != undefined
439
441
  }
440
442
 
443
+ /**
444
+ * Pulls data from storage to build a valid beef for a txid.
445
+ *
446
+ * Optionally merges the data into an existing beef.
447
+ * Optionally requires a minimum number of proof levels.
448
+ *
449
+ * @param txid
450
+ * @param mergeToBeef
451
+ * @param trustSelf
452
+ * @param knownTxids
453
+ * @param trx
454
+ * @param requiredLevels
455
+ * @returns
456
+ */
441
457
  async getValidBeefForKnownTxid(
442
458
  txid: string,
443
459
  mergeToBeef?: Beef,
@@ -457,7 +473,9 @@ export abstract class StorageProvider extends StorageReaderWriter implements Wal
457
473
  trustSelf?: TrustSelf,
458
474
  knownTxids?: string[],
459
475
  trx?: TrxToken,
460
- requiredLevels?: number
476
+ requiredLevels?: number,
477
+ chainTracker?: ChainTracker,
478
+ skipInvalidProofs?: boolean
461
479
  ): Promise<Beef | undefined> {
462
480
  const beef = mergeToBeef || new Beef()
463
481
 
@@ -468,10 +486,25 @@ export abstract class StorageProvider extends StorageReaderWriter implements Wal
468
486
  } else {
469
487
  if (trustSelf === 'known') beef.mergeTxidOnly(txid)
470
488
  else {
471
- beef.mergeRawTx(r.proven.rawTx)
472
489
  const mp = new EntityProvenTx(r.proven).getMerklePath()
473
- beef.mergeBump(mp)
474
- return beef
490
+ if (chainTracker) {
491
+ const root = mp.computeRoot()
492
+ const isValid = await chainTracker.isValidRootForHeight(root, r.proven.height)
493
+ if (!isValid) {
494
+ if (!skipInvalidProofs) {
495
+ throw new WERR_INVALID_MERKLE_ROOT(r.proven.blockHash, r.proven.height, root, txid)
496
+ }
497
+ // ignore this currently invalid proof and try to recurse deeper
498
+ r.rawTx = r.proven.rawTx
499
+ r.proven = undefined
500
+ }
501
+ }
502
+ if (r.proven) {
503
+ // If we still like this proof, merge it and return
504
+ beef.mergeRawTx(r.proven.rawTx)
505
+ beef.mergeBump(mp)
506
+ return beef
507
+ }
475
508
  }
476
509
  }
477
510
  }