@bsv/wallet-toolbox 1.6.14 → 1.6.16

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 (25) hide show
  1. package/docs/client.md +123 -93
  2. package/docs/services.md +123 -93
  3. package/docs/wallet.md +123 -93
  4. package/mobile/out/src/services/chaintracker/chaintracks/Chaintracks.d.ts +1 -0
  5. package/mobile/out/src/services/chaintracker/chaintracks/Chaintracks.d.ts.map +1 -1
  6. package/mobile/out/src/services/chaintracker/chaintracks/Chaintracks.js +125 -109
  7. package/mobile/out/src/services/chaintracker/chaintracks/Chaintracks.js.map +1 -1
  8. package/mobile/out/src/services/chaintracker/chaintracks/util/BulkFileDataManager.d.ts +16 -4
  9. package/mobile/out/src/services/chaintracker/chaintracks/util/BulkFileDataManager.d.ts.map +1 -1
  10. package/mobile/out/src/services/chaintracker/chaintracks/util/BulkFileDataManager.js +65 -45
  11. package/mobile/out/src/services/chaintracker/chaintracks/util/BulkFileDataManager.js.map +1 -1
  12. package/mobile/package-lock.json +2 -2
  13. package/mobile/package.json +1 -1
  14. package/out/src/services/chaintracker/chaintracks/Chaintracks.d.ts +1 -0
  15. package/out/src/services/chaintracker/chaintracks/Chaintracks.d.ts.map +1 -1
  16. package/out/src/services/chaintracker/chaintracks/Chaintracks.js +125 -109
  17. package/out/src/services/chaintracker/chaintracks/Chaintracks.js.map +1 -1
  18. package/out/src/services/chaintracker/chaintracks/util/BulkFileDataManager.d.ts +16 -4
  19. package/out/src/services/chaintracker/chaintracks/util/BulkFileDataManager.d.ts.map +1 -1
  20. package/out/src/services/chaintracker/chaintracks/util/BulkFileDataManager.js +65 -45
  21. package/out/src/services/chaintracker/chaintracks/util/BulkFileDataManager.js.map +1 -1
  22. package/out/tsconfig.all.tsbuildinfo +1 -1
  23. package/package.json +1 -1
  24. package/src/services/chaintracker/chaintracks/Chaintracks.ts +124 -109
  25. package/src/services/chaintracker/chaintracks/util/BulkFileDataManager.ts +75 -50
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/wallet-toolbox",
3
- "version": "1.6.14",
3
+ "version": "1.6.16",
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",
@@ -14,6 +14,7 @@ import { HeightRange, HeightRanges } from './util/HeightRange'
14
14
  import { SingleWriterMultiReaderLock } from './util/SingleWriterMultiReaderLock'
15
15
  import { ChaintracksFsApi } from './Api/ChaintracksFsApi'
16
16
  import { randomBytesBase64, wait } from '../../../utility/utilityHelpers'
17
+ import { WalletError } from '../../../sdk/WalletError'
17
18
 
18
19
  export class Chaintracks implements ChaintracksManagementApi {
19
20
  static createOptions(chain: Chain): ChaintracksOptions {
@@ -47,6 +48,7 @@ export class Chaintracks implements ChaintracksManagementApi {
47
48
  private addLiveRecursionLimit = 11
48
49
 
49
50
  private available = false
51
+ private startupError: WalletError | null = null
50
52
 
51
53
  private subscriberCallbacksEnabled = false
52
54
  private stopMainThread = true
@@ -169,9 +171,11 @@ export class Chaintracks implements ChaintracksManagementApi {
169
171
  this.promises.push(this.mainThreadShiftLiveHeaders())
170
172
 
171
173
  // Wait for the main thread to finish initial sync.
172
- while (!this.available) {
174
+ while (!this.available && !this.startupError) {
173
175
  await wait(100)
174
176
  }
177
+
178
+ if (this.startupError) throw this.startupError
175
179
  })
176
180
  }
177
181
 
@@ -432,147 +436,158 @@ export class Chaintracks implements ChaintracksManagementApi {
432
436
  const syncCheckRepeatMsecs = 30 * 60 * 1000 // 30 minutes
433
437
 
434
438
  while (!this.stopMainThread) {
435
- // Review the need for bulk sync...
436
- const now = Date.now()
437
- lastSyncCheck = now
439
+ try {
440
+ // Review the need for bulk sync...
441
+ const now = Date.now()
442
+ lastSyncCheck = now
438
443
 
439
- const presentHeight = await this.getPresentHeight()
440
- const before = await this.storage.getAvailableHeightRanges()
444
+ const presentHeight = await this.getPresentHeight()
445
+ const before = await this.storage.getAvailableHeightRanges()
441
446
 
442
- // Skip bulk sync if within less than half the recursion limit of present height
443
- let skipBulkSync = !before.live.isEmpty && before.live.maxHeight >= presentHeight - this.addLiveRecursionLimit / 2
447
+ // Skip bulk sync if within less than half the recursion limit of present height
448
+ let skipBulkSync =
449
+ !before.live.isEmpty && before.live.maxHeight >= presentHeight - this.addLiveRecursionLimit / 2
444
450
 
445
- if (skipBulkSync && now - lastSyncCheck > cdnSyncRepeatMsecs) {
446
- // If we haven't re-synced in a long time, do it just to check for a CDN update.
447
- skipBulkSync = false
448
- }
451
+ if (skipBulkSync && now - lastSyncCheck > cdnSyncRepeatMsecs) {
452
+ // If we haven't re-synced in a long time, do it just to check for a CDN update.
453
+ skipBulkSync = false
454
+ }
449
455
 
450
- this.log(`Chaintracks Update Services: Bulk Header Sync Review
456
+ this.log(`Chaintracks Update Services: Bulk Header Sync Review
451
457
  presentHeight=${presentHeight} addLiveRecursionLimit=${this.addLiveRecursionLimit}
452
458
  Before synchronize: bulk ${before.bulk}, live ${before.live}
453
459
  ${skipBulkSync ? 'Skipping' : 'Starting'} syncBulkStorage.
454
460
  `)
455
461
 
456
- if (!skipBulkSync) {
457
- // Bring bulk storage up-to-date and (re-)initialize liveHeaders
458
- lastBulkSync = now
459
- if (this.available)
460
- // Once available, initial write lock is released, take a new one to update bulk storage.
461
- await this.syncBulkStorage(presentHeight, before)
462
- else
463
- // While still not available, the makeAvailable write lock is held.
464
- await this.syncBulkStorageNoLock(presentHeight, before)
465
- }
462
+ if (!skipBulkSync) {
463
+ // Bring bulk storage up-to-date and (re-)initialize liveHeaders
464
+ lastBulkSync = now
465
+ if (this.available)
466
+ // Once available, initial write lock is released, take a new one to update bulk storage.
467
+ await this.syncBulkStorage(presentHeight, before)
468
+ else
469
+ // While still not available, the makeAvailable write lock is held.
470
+ await this.syncBulkStorageNoLock(presentHeight, before)
471
+ }
466
472
 
467
- let count = 0
468
- let liveHeaderDupes = 0
469
- let needSyncCheck = false
470
-
471
- for (; !needSyncCheck && !this.stopMainThread; ) {
472
- let header = this.liveHeaders.shift()
473
- if (header) {
474
- // Process a "live" block header...
475
- let recursions = this.addLiveRecursionLimit
476
- for (; !needSyncCheck && !this.stopMainThread; ) {
477
- //console.log(`Processing liveHeader: height: ${header.height} hash: ${header.hash} ${new Date().toISOString()}`)
478
- const ihr = await this.addLiveHeader(header)
479
- if (this.invalidInsertHeaderResult(ihr)) {
480
- this.log(`Ignoring liveHeader ${header.height} ${header.hash} due to invalid insert result.`)
481
- needSyncCheck = true
482
- } else if (ihr.noPrev) {
483
- // Previous header is unknown, request it by hash from the network and try adding it first...
484
- if (recursions-- <= 0) {
485
- // Ignore this header...
486
- this.log(
487
- `Ignoring liveHeader ${header.height} ${header.hash} addLiveRecursionLimit=${this.addLiveRecursionLimit} exceeded.`
488
- )
473
+ let count = 0
474
+ let liveHeaderDupes = 0
475
+ let needSyncCheck = false
476
+
477
+ for (; !needSyncCheck && !this.stopMainThread; ) {
478
+ let header = this.liveHeaders.shift()
479
+ if (header) {
480
+ // Process a "live" block header...
481
+ let recursions = this.addLiveRecursionLimit
482
+ for (; !needSyncCheck && !this.stopMainThread; ) {
483
+ //console.log(`Processing liveHeader: height: ${header.height} hash: ${header.hash} ${new Date().toISOString()}`)
484
+ const ihr = await this.addLiveHeader(header)
485
+ if (this.invalidInsertHeaderResult(ihr)) {
486
+ this.log(`Ignoring liveHeader ${header.height} ${header.hash} due to invalid insert result.`)
489
487
  needSyncCheck = true
490
- } else {
491
- const hash = header.previousHash
492
- const prevHeader = await this.getMissingBlockHeader(hash)
493
- if (!prevHeader) {
488
+ } else if (ihr.noPrev) {
489
+ // Previous header is unknown, request it by hash from the network and try adding it first...
490
+ if (recursions-- <= 0) {
491
+ // Ignore this header...
494
492
  this.log(
495
- `Ignoring liveHeader ${header.height} ${header.hash} failed to find previous header by hash ${asString(hash)}`
493
+ `Ignoring liveHeader ${header.height} ${header.hash} addLiveRecursionLimit=${this.addLiveRecursionLimit} exceeded.`
496
494
  )
497
495
  needSyncCheck = true
498
496
  } else {
499
- // Switch to trying to add prevHeader, unshifting current header to try it again after prevHeader exists.
500
- this.liveHeaders.unshift(header)
501
- header = prevHeader
497
+ const hash = header.previousHash
498
+ const prevHeader = await this.getMissingBlockHeader(hash)
499
+ if (!prevHeader) {
500
+ this.log(
501
+ `Ignoring liveHeader ${header.height} ${header.hash} failed to find previous header by hash ${asString(hash)}`
502
+ )
503
+ needSyncCheck = true
504
+ } else {
505
+ // Switch to trying to add prevHeader, unshifting current header to try it again after prevHeader exists.
506
+ this.liveHeaders.unshift(header)
507
+ header = prevHeader
508
+ }
502
509
  }
503
- }
504
- } else {
505
- if (this.subscriberCallbacksEnabled)
506
- this.log(
507
- `addLiveHeader ${header.height}${ihr.added ? ' added' : ''}${ihr.dupe ? ' dupe' : ''}${ihr.isActiveTip ? ' isActiveTip' : ''}${ihr.reorgDepth ? ' reorg depth ' + ihr.reorgDepth : ''}${ihr.noPrev ? ' noPrev' : ''}${ihr.noActiveAncestor || ihr.noTip || ihr.badPrev ? ' error' : ''}`
508
- )
509
- if (ihr.dupe) {
510
- liveHeaderDupes++
511
- }
512
- // Header wasn't invalid and previous header is known. If it was successfully added, count it as a win.
513
- if (ihr.added) {
514
- count++
515
- }
516
- break
517
- }
518
- }
519
- } else {
520
- // There are no liveHeaders currently to process, check the out-of-band baseHeaders channel (`addHeader` method called by a client).
521
- const bheader = this.baseHeaders.shift()
522
- if (bheader) {
523
- const prev = await this.storage.findLiveHeaderForBlockHash(bheader.previousHash)
524
- if (!prev) {
525
- // Ignoring attempt to add a baseHeader with unknown previous hash, no attempt made to find previous header(s).
526
- this.log(`Ignoring header with unknown previousHash ${bheader.previousHash} in live storage.`)
527
- // Does not trigger a re-sync.
528
- } else {
529
- const header: BlockHeader = {
530
- ...bheader,
531
- height: prev.height + 1,
532
- hash: blockHash(bheader)
533
- }
534
- const ihr = await this.addLiveHeader(header)
535
- if (this.invalidInsertHeaderResult(ihr)) {
536
- this.log(`Ignoring invalid baseHeader ${header.height} ${header.hash}.`)
537
510
  } else {
538
511
  if (this.subscriberCallbacksEnabled)
539
512
  this.log(
540
- `addBaseHeader ${header.height}${ihr.added ? ' added' : ''}${ihr.dupe ? ' dupe' : ''}${ihr.isActiveTip ? ' isActiveTip' : ''}${ihr.reorgDepth ? ' reorg depth ' + ihr.reorgDepth : ''}${ihr.noPrev ? ' noPrev' : ''}${ihr.noActiveAncestor || ihr.noTip || ihr.badPrev ? ' error' : ''}`
513
+ `addLiveHeader ${header.height}${ihr.added ? ' added' : ''}${ihr.dupe ? ' dupe' : ''}${ihr.isActiveTip ? ' isActiveTip' : ''}${ihr.reorgDepth ? ' reorg depth ' + ihr.reorgDepth : ''}${ihr.noPrev ? ' noPrev' : ''}${ihr.noActiveAncestor || ihr.noTip || ihr.badPrev ? ' error' : ''}`
541
514
  )
542
- // baseHeader was successfully added.
515
+ if (ihr.dupe) {
516
+ liveHeaderDupes++
517
+ }
518
+ // Header wasn't invalid and previous header is known. If it was successfully added, count it as a win.
543
519
  if (ihr.added) {
544
520
  count++
545
521
  }
522
+ break
546
523
  }
547
524
  }
548
525
  } else {
549
- // There are no liveHeaders and no baseHeaders to add,
550
- if (count > 0) {
551
- if (liveHeaderDupes > 0) {
552
- this.log(`${liveHeaderDupes} duplicate headers ignored.`)
553
- liveHeaderDupes = 0
526
+ // There are no liveHeaders currently to process, check the out-of-band baseHeaders channel (`addHeader` method called by a client).
527
+ const bheader = this.baseHeaders.shift()
528
+ if (bheader) {
529
+ const prev = await this.storage.findLiveHeaderForBlockHash(bheader.previousHash)
530
+ if (!prev) {
531
+ // Ignoring attempt to add a baseHeader with unknown previous hash, no attempt made to find previous header(s).
532
+ this.log(`Ignoring header with unknown previousHash ${bheader.previousHash} in live storage.`)
533
+ // Does not trigger a re-sync.
534
+ } else {
535
+ const header: BlockHeader = {
536
+ ...bheader,
537
+ height: prev.height + 1,
538
+ hash: blockHash(bheader)
539
+ }
540
+ const ihr = await this.addLiveHeader(header)
541
+ if (this.invalidInsertHeaderResult(ihr)) {
542
+ this.log(`Ignoring invalid baseHeader ${header.height} ${header.hash}.`)
543
+ } else {
544
+ if (this.subscriberCallbacksEnabled)
545
+ this.log(
546
+ `addBaseHeader ${header.height}${ihr.added ? ' added' : ''}${ihr.dupe ? ' dupe' : ''}${ihr.isActiveTip ? ' isActiveTip' : ''}${ihr.reorgDepth ? ' reorg depth ' + ihr.reorgDepth : ''}${ihr.noPrev ? ' noPrev' : ''}${ihr.noActiveAncestor || ihr.noTip || ihr.badPrev ? ' error' : ''}`
547
+ )
548
+ // baseHeader was successfully added.
549
+ if (ihr.added) {
550
+ count++
551
+ }
552
+ }
554
553
  }
555
- const updated = await this.storage.getAvailableHeightRanges()
556
- this.log(`After adding ${count} live headers
554
+ } else {
555
+ // There are no liveHeaders and no baseHeaders to add,
556
+ if (count > 0) {
557
+ if (liveHeaderDupes > 0) {
558
+ this.log(`${liveHeaderDupes} duplicate headers ignored.`)
559
+ liveHeaderDupes = 0
560
+ }
561
+ const updated = await this.storage.getAvailableHeightRanges()
562
+ this.log(`After adding ${count} live headers
557
563
  After live: bulk ${updated.bulk}, live ${updated.live}
558
564
  `)
559
- count = 0
560
- }
561
- if (!this.subscriberCallbacksEnabled) {
562
- const live = await this.storage.findLiveHeightRange()
563
- if (!live.isEmpty) {
564
- this.subscriberCallbacksEnabled = true
565
- this.log(`listening at height of ${live.maxHeight}`)
565
+ count = 0
566
566
  }
567
+ if (!this.subscriberCallbacksEnabled) {
568
+ const live = await this.storage.findLiveHeightRange()
569
+ if (!live.isEmpty) {
570
+ this.subscriberCallbacksEnabled = true
571
+ this.log(`listening at height of ${live.maxHeight}`)
572
+ }
573
+ }
574
+ if (!this.available) {
575
+ this.available = true
576
+ }
577
+ needSyncCheck = Date.now() - lastSyncCheck > syncCheckRepeatMsecs
578
+ // If we aren't going to review sync, wait before checking input queues again
579
+ if (!needSyncCheck) await wait(1000)
567
580
  }
568
- if (!this.available) {
569
- this.available = true
570
- }
571
- needSyncCheck = Date.now() - lastSyncCheck > syncCheckRepeatMsecs
572
- // If we aren't going to review sync, wait before checking input queues again
573
- if (!needSyncCheck) await wait(1000)
574
581
  }
575
582
  }
583
+ } catch (eu: unknown) {
584
+ const e = WalletError.fromUnknown(eu)
585
+ if (!this.available) {
586
+ this.startupError = e
587
+ this.stopMainThread = true
588
+ } else {
589
+ this.log(`Error occurred during chaintracks main thread processing: ${e.stack || e.message}`)
590
+ }
576
591
  }
577
592
  }
578
593
  }
@@ -72,6 +72,81 @@ export class BulkFileDataManager {
72
72
  this.deleteBulkFilesNoLock()
73
73
  }
74
74
 
75
+ async deleteBulkFiles(): Promise<void> {
76
+ return this.lock.withWriteLock(async () => this.deleteBulkFilesNoLock())
77
+ }
78
+
79
+ private deleteBulkFilesNoLock(): void {
80
+ this.bfds = []
81
+ this.fileHashToIndex = {}
82
+
83
+ if (this.fromKnownSourceUrl) {
84
+ const vbhfs = validBulkHeaderFiles
85
+ const filtered = vbhfs.filter(f => f.sourceUrl === this.fromKnownSourceUrl)
86
+ const files = selectBulkHeaderFiles(filtered, this.chain, this.maxPerFile)
87
+ for (const file of files) {
88
+ this.add({ ...file, fileHash: file.fileHash!, mru: Date.now() })
89
+ }
90
+ }
91
+ }
92
+
93
+ /**
94
+ * If `bfds` are going to be backed by persistent storage,
95
+ * must be called before making storage available.
96
+ *
97
+ * Synchronizes bfds and storage files, after which this manager maintains sync.
98
+ * There should be no changes to bulk files by direct access to storage bulk file methods.
99
+ */
100
+ async setStorage(storage: ChaintracksStorageBulkFileApi, log: (...args: any[]) => void): Promise<void> {
101
+ return this.lock.withWriteLock(async () => this.setStorageNoLock(storage, log))
102
+ }
103
+
104
+ private async setStorageNoLock(storage: ChaintracksStorageBulkFileApi, log: (...args: any[]) => void): Promise<void> {
105
+ this.storage = storage
106
+ this.log = log
107
+
108
+ // Get files currently in persistent storage.
109
+ let sfs = await this.storage.getBulkFiles()
110
+
111
+ // Sync bfds with storage. Two scenarios supported:
112
+
113
+ const bfdsRanges = this.heightRangesFromBulkFiles(this.bfds)
114
+ const sfsRanges = this.heightRangesFromBulkFiles(sfs)
115
+
116
+ if (sfsRanges.cdn.length >= bfdsRanges.cdn.length) {
117
+ // Storage win if it has greater or equal CDN coverage
118
+ // Replace all bfds with sfs
119
+ this.bfds = []
120
+ for (const file of sfs) {
121
+ const vbf: BulkFileData = await this.validateFileInfo(file)
122
+ this.bfds.push(vbf)
123
+ }
124
+ } else {
125
+ // Bfds win if they have greater CDN coverage
126
+ // Replace all sfs with bfds
127
+ for (const s of sfs.reverse()) await this.storage.deleteBulkFile(s.fileId!)
128
+ for (const bfd of this.bfds) {
129
+ await this.ensureData(bfd)
130
+ bfd.fileId = await this.storage.insertBulkFile(bfdToInfo(bfd, true))
131
+ }
132
+ }
133
+ }
134
+
135
+ heightRangesFromBulkFiles(files: BulkHeaderFileInfo[]): {
136
+ all: HeightRange
137
+ cdn: HeightRange
138
+ incremental: HeightRange
139
+ } {
140
+ const ranges = { all: new HeightRange(0, -1), cdn: new HeightRange(0, -1), incremental: new HeightRange(0, -1) }
141
+ for (const file of files) {
142
+ const range = new HeightRange(file.firstHeight, file.firstHeight + file.count - 1)
143
+ ranges.all = ranges.all.union(range)
144
+ if (isBdfCdn(file)) ranges.cdn = ranges.cdn.union(range)
145
+ if (isBdfIncremental(file)) ranges.incremental = ranges.incremental.union(range)
146
+ }
147
+ return ranges
148
+ }
149
+
75
150
  async createReader(range?: HeightRange, maxBufferSize?: number): Promise<BulkFileDataReader> {
76
151
  range = range || (await this.getHeightRange())
77
152
  maxBufferSize = maxBufferSize || 1000000 * 80 // 100,000 headers, 8MB
@@ -114,56 +189,6 @@ export class BulkFileDataManager {
114
189
  this.log(log)
115
190
  }
116
191
 
117
- async setStorage(storage: ChaintracksStorageBulkFileApi, log: (...args: any[]) => void): Promise<void> {
118
- return this.lock.withWriteLock(async () => this.setStorageNoLock(storage, log))
119
- }
120
-
121
- private async setStorageNoLock(storage: ChaintracksStorageBulkFileApi, log: (...args: any[]) => void): Promise<void> {
122
- this.storage = storage
123
- this.log = log
124
- // Sync bfds with storage. Two scenarios supported:
125
- let sfs = await this.storage.getBulkFiles()
126
- const lastCdnBfd = this.bfds.filter(f => isBdfCdn(f)).slice(-1)[0]
127
- const lastCdnSfs = sfs.filter(f => isBdfCdn(f)).slice(-1)[0]
128
- if (
129
- lastCdnBfd &&
130
- lastCdnSfs &&
131
- lastCdnBfd.firstHeight + lastCdnBfd.count > lastCdnSfs.firstHeight + lastCdnSfs.count
132
- ) {
133
- // Storage has fewer cdn headers than bfds, clear them and try again.
134
- for (const s of sfs.reverse()) await this.storage.deleteBulkFile(s.fileId!)
135
- sfs = []
136
- }
137
- if (sfs.length === 0) {
138
- // 1. Storage has no files: Update storage to reflect bfds.
139
- for (const bfd of this.bfds) {
140
- await this.ensureData(bfd)
141
- bfd.fileId = await this.storage.insertBulkFile(bfdToInfo(bfd, true))
142
- }
143
- } else {
144
- // 2. bfds are a prefix of storage, including last bfd having same firstHeight but possibly fewer headers: Merge storage to bfds.
145
- const r = await this.mergeNoLock(sfs)
146
- }
147
- }
148
-
149
- async deleteBulkFiles(): Promise<void> {
150
- return this.lock.withWriteLock(async () => this.deleteBulkFilesNoLock())
151
- }
152
-
153
- private deleteBulkFilesNoLock(): void {
154
- this.bfds = []
155
- this.fileHashToIndex = {}
156
-
157
- if (this.fromKnownSourceUrl) {
158
- const vbhfs = validBulkHeaderFiles
159
- const filtered = vbhfs.filter(f => f.sourceUrl === this.fromKnownSourceUrl)
160
- const files = selectBulkHeaderFiles(filtered, this.chain, this.maxPerFile)
161
- for (const file of files) {
162
- this.add({ ...file, fileHash: file.fileHash!, mru: Date.now() })
163
- }
164
- }
165
- }
166
-
167
192
  async merge(files: BulkHeaderFileInfo[]): Promise<BulkFileDataManagerMergeResult> {
168
193
  return this.lock.withWriteLock(async () => this.mergeNoLock(files))
169
194
  }