@bsv/wallet-toolbox 1.7.18 → 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 (38) hide show
  1. package/docs/client.md +132 -57
  2. package/docs/services.md +13 -1
  3. package/docs/storage.md +13 -1
  4. package/docs/wallet.md +132 -57
  5. package/out/src/WalletPermissionsManager.d.ts +48 -1
  6. package/out/src/WalletPermissionsManager.d.ts.map +1 -1
  7. package/out/src/WalletPermissionsManager.js +775 -124
  8. package/out/src/WalletPermissionsManager.js.map +1 -1
  9. package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts.map +1 -1
  10. package/out/src/__tests/WalletPermissionsManager.fixtures.js +9 -0
  11. package/out/src/__tests/WalletPermissionsManager.fixtures.js.map +1 -1
  12. package/out/src/__tests/WalletPermissionsManager.flows.test.js +121 -0
  13. package/out/src/__tests/WalletPermissionsManager.flows.test.js.map +1 -1
  14. package/out/src/services/chaintracker/chaintracks/Ingest/BulkIngestorWhatsOnChainCdn.d.ts +1 -1
  15. package/out/src/services/chaintracker/chaintracks/__tests/Chaintracks.test.js +3 -1
  16. package/out/src/services/chaintracker/chaintracks/__tests/Chaintracks.test.js.map +1 -1
  17. package/out/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.d.ts.map +1 -1
  18. package/out/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.js +12 -0
  19. package/out/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.js.map +1 -1
  20. package/out/src/storage/StorageProvider.d.ts +16 -2
  21. package/out/src/storage/StorageProvider.d.ts.map +1 -1
  22. package/out/src/storage/StorageProvider.js +33 -4
  23. package/out/src/storage/StorageProvider.js.map +1 -1
  24. package/out/src/storage/methods/__test/offsetKey.test.js +266 -100
  25. package/out/src/storage/methods/__test/offsetKey.test.js.map +1 -1
  26. package/out/src/storage/methods/getBeefForTransaction.js +1 -1
  27. package/out/src/storage/methods/getBeefForTransaction.js.map +1 -1
  28. package/out/tsconfig.all.tsbuildinfo +1 -1
  29. package/package.json +2 -2
  30. package/src/WalletPermissionsManager.ts +973 -129
  31. package/src/__tests/WalletPermissionsManager.fixtures.ts +9 -0
  32. package/src/__tests/WalletPermissionsManager.flows.test.ts +157 -0
  33. package/src/services/chaintracker/chaintracks/Ingest/BulkIngestorWhatsOnChainCdn.ts +1 -1
  34. package/src/services/chaintracker/chaintracks/__tests/Chaintracks.test.ts +2 -1
  35. package/src/services/chaintracker/chaintracks/util/validBulkHeaderFilesByFileHash.ts +12 -0
  36. package/src/storage/StorageProvider.ts +38 -5
  37. package/src/storage/methods/__test/offsetKey.test.ts +299 -103
  38. package/src/storage/methods/getBeefForTransaction.ts +3 -1
@@ -1,5 +1,14 @@
1
1
  const { Validation } = jest.requireActual('@bsv/sdk')
2
2
 
3
+ const existingFetch = (globalThis as any).fetch
4
+ if (!existingFetch || !(existingFetch as any)._isMockFunction) {
5
+ ;(globalThis as any).fetch = jest.fn(async () => ({
6
+ ok: false,
7
+ status: 404,
8
+ json: async () => ({})
9
+ }))
10
+ }
11
+
3
12
  /**
4
13
  * A permissions manager testing mock/stub file for:
5
14
  * 1) The `@bsv/sdk` library: Transaction, LockingScript, PushDrop, Utils, Random, etc.
@@ -35,6 +35,163 @@ describe('WalletPermissionsManager - Permission Request Flow & Active Requests',
35
35
  * UNIT TESTS
36
36
  */
37
37
  describe('Unit Tests: requestPermissionFlow & activeRequests map', () => {
38
+ it('should coalesce level-2 protocol requests for the same counterparty into a single grouped permission prompt based on manifest.json', async () => {
39
+ mockNoTokensFound(manager)
40
+
41
+ jest.spyOn(manager as any, 'fetchManifestGroupPermissions').mockResolvedValue({
42
+ protocolPermissions: [
43
+ {
44
+ protocolID: [2, 'l2-proto-A'],
45
+ counterparty: '',
46
+ description: 'A'
47
+ },
48
+ {
49
+ protocolID: [2, 'l2-proto-B'],
50
+ counterparty: 'peer-123',
51
+ description: 'B'
52
+ },
53
+ {
54
+ protocolID: [2, 'l2-proto-C'],
55
+ counterparty: 'peer-999',
56
+ description: 'C'
57
+ },
58
+ {
59
+ protocolID: [1, 'l1-proto-D'],
60
+ counterparty: 'peer-123',
61
+ description: 'D'
62
+ }
63
+ ]
64
+ })
65
+
66
+ const groupRequestCallback = jest.fn(() => {})
67
+ manager.bindCallback('onGroupedPermissionRequested', groupRequestCallback)
68
+
69
+ const callA = manager.ensureProtocolPermission({
70
+ originator: 'example.com',
71
+ privileged: false,
72
+ protocolID: [2, 'l2-proto-A'],
73
+ counterparty: 'peer-123',
74
+ reason: 'UnitTest - L2 A',
75
+ seekPermission: true,
76
+ usageType: 'signing'
77
+ })
78
+
79
+ const callB = manager.ensureProtocolPermission({
80
+ originator: 'example.com',
81
+ privileged: false,
82
+ protocolID: [2, 'l2-proto-B'],
83
+ counterparty: 'peer-123',
84
+ reason: 'UnitTest - L2 B',
85
+ seekPermission: true,
86
+ usageType: 'signing'
87
+ })
88
+
89
+ await new Promise(res => setTimeout(res, 5))
90
+
91
+ expect(groupRequestCallback).toHaveBeenCalledTimes(1)
92
+ const callbackArg = (groupRequestCallback.mock as any).calls[0][0]
93
+ const requestID = callbackArg.requestID
94
+ expect(typeof requestID).toBe('string')
95
+ expect(requestID).toMatch(/^group-peer:/)
96
+
97
+ const activeRequests = (manager as any).activeRequests as Map<string, any>
98
+ const queued = activeRequests.get(requestID)
99
+ expect(queued.request.permissions.protocolPermissions.length).toBe(2)
100
+ expect(queued.request.permissions.protocolPermissions).toEqual(
101
+ expect.arrayContaining([
102
+ expect.objectContaining({ protocolID: [2, 'l2-proto-A'], counterparty: 'peer-123' }),
103
+ expect.objectContaining({ protocolID: [2, 'l2-proto-B'], counterparty: 'peer-123' })
104
+ ])
105
+ )
106
+
107
+ await manager.denyGroupedPermission(requestID)
108
+
109
+ await expect(callA).rejects.toThrow(/denied/i)
110
+ await expect(callB).rejects.toThrow(/denied/i)
111
+
112
+ expect(activeRequests.size).toBe(0)
113
+ })
114
+
115
+ it('should create separate grouped permission requests for different peers (no cross-peer grouping)', async () => {
116
+ mockNoTokensFound(manager)
117
+
118
+ jest.spyOn(manager as any, 'fetchManifestGroupPermissions').mockResolvedValue({
119
+ protocolPermissions: [
120
+ {
121
+ protocolID: [2, 'l2-proto-B'],
122
+ counterparty: 'peer-123',
123
+ description: 'B'
124
+ },
125
+ {
126
+ protocolID: [2, 'l2-proto-C'],
127
+ counterparty: 'peer-999',
128
+ description: 'C'
129
+ }
130
+ ]
131
+ })
132
+
133
+ const groupRequestCallback = jest.fn(() => {})
134
+ manager.bindCallback('onGroupedPermissionRequested', groupRequestCallback)
135
+
136
+ const callB = manager.ensureProtocolPermission({
137
+ originator: 'example.com',
138
+ privileged: false,
139
+ protocolID: [2, 'l2-proto-B'],
140
+ counterparty: 'peer-123',
141
+ reason: 'UnitTest - L2 B peer-123',
142
+ seekPermission: true,
143
+ usageType: 'signing'
144
+ })
145
+
146
+ const callC = manager.ensureProtocolPermission({
147
+ originator: 'example.com',
148
+ privileged: false,
149
+ protocolID: [2, 'l2-proto-C'],
150
+ counterparty: 'peer-999',
151
+ reason: 'UnitTest - L2 C peer-999',
152
+ seekPermission: true,
153
+ usageType: 'signing'
154
+ })
155
+
156
+ await new Promise(res => setTimeout(res, 5))
157
+
158
+ expect(groupRequestCallback).toHaveBeenCalledTimes(2)
159
+ const requestID1 = (groupRequestCallback.mock as any).calls[0][0].requestID
160
+ const requestID2 = (groupRequestCallback.mock as any).calls[1][0].requestID
161
+
162
+ expect(requestID1).not.toBe(requestID2)
163
+ expect(requestID1).toMatch(/^group-peer:/)
164
+ expect(requestID2).toMatch(/^group-peer:/)
165
+
166
+ const activeRequests = (manager as any).activeRequests as Map<string, any>
167
+ expect(activeRequests.size).toBe(2)
168
+
169
+ const queued1 = activeRequests.get(requestID1)
170
+ const queued2 = activeRequests.get(requestID2)
171
+
172
+ expect(queued1.request.permissions.protocolPermissions).toEqual(
173
+ expect.arrayContaining([expect.objectContaining({ protocolID: [2, 'l2-proto-B'], counterparty: 'peer-123' })])
174
+ )
175
+ expect(queued1.request.permissions.protocolPermissions).not.toEqual(
176
+ expect.arrayContaining([expect.objectContaining({ protocolID: [2, 'l2-proto-C'], counterparty: 'peer-999' })])
177
+ )
178
+
179
+ expect(queued2.request.permissions.protocolPermissions).toEqual(
180
+ expect.arrayContaining([expect.objectContaining({ protocolID: [2, 'l2-proto-C'], counterparty: 'peer-999' })])
181
+ )
182
+ expect(queued2.request.permissions.protocolPermissions).not.toEqual(
183
+ expect.arrayContaining([expect.objectContaining({ protocolID: [2, 'l2-proto-B'], counterparty: 'peer-123' })])
184
+ )
185
+
186
+ await manager.denyGroupedPermission(requestID1)
187
+ await manager.denyGroupedPermission(requestID2)
188
+
189
+ await expect(callB).rejects.toThrow(/denied/i)
190
+ await expect(callC).rejects.toThrow(/denied/i)
191
+
192
+ expect(activeRequests.size).toBe(0)
193
+ })
194
+
38
195
  it('should coalesce parallel requests for the same resource into a single user prompt', async () => {
39
196
  // We want to test the underlying private method "requestPermissionFlow" indirectly
40
197
  // or we can test it via a public method that calls it. We'll do so via ensureProtocolPermission.
@@ -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
  }