@bopen-io/wallet-toolbox 1.7.19 → 1.7.20-idb-fix.1

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bopen-io/wallet-toolbox",
3
- "version": "1.7.19",
3
+ "version": "1.7.20-idb-fix.1",
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",
@@ -33,7 +33,7 @@
33
33
  "dependencies": {
34
34
  "@bsv/auth-express-middleware": "^1.2.3",
35
35
  "@bsv/payment-express-middleware": "^1.2.3",
36
- "@bsv/sdk": "^1.9.29",
36
+ "@bsv/sdk": "^1.10.1",
37
37
  "express": "^4.21.2",
38
38
  "idb": "^8.0.2",
39
39
  "knex": "^3.1.0",
@@ -114,6 +114,17 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
114
114
  ): IDBPTransaction<StorageIdbSchema, string[], 'readwrite' | 'readonly'> {
115
115
  if (trx) {
116
116
  const t = trx as IDBPTransaction<StorageIdbSchema, string[], 'readwrite' | 'readonly'>
117
+ // Check if the transaction is still active by trying to access an object store
118
+ try {
119
+ const storeToCheck = stores[0] || this.allStores[0]
120
+ t.objectStore(storeToCheck)
121
+ } catch (e) {
122
+ console.error(
123
+ `[StorageIdb.toDbTrx] Passed transaction already finished! stores=${stores.join(',')}, mode=${mode}`
124
+ )
125
+ console.error('[StorageIdb.toDbTrx] Stack trace:', new Error().stack)
126
+ throw e
127
+ }
117
128
  return t
118
129
  } else {
119
130
  if (!this.db) throw new Error('not initialized')
@@ -357,7 +368,8 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
357
368
  excludeSending: boolean,
358
369
  transactionId: number
359
370
  ): Promise<TableOutput | undefined> {
360
- const dbTrx = this.toDbTrx(['outputs', 'transactions'], 'readwrite')
371
+ // Include proven_txs in store list since findOutputs -> validateOutputScript needs it
372
+ const dbTrx = this.toDbTrx(['outputs', 'transactions', 'proven_txs'], 'readwrite')
361
373
  try {
362
374
  const txStatus: TransactionStatus[] = ['completed', 'unproven']
363
375
  if (!excludeSending) txStatus.push('sending')
@@ -519,6 +531,17 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
519
531
  filtered: (v: TableOutputTagMap) => void,
520
532
  userId?: number
521
533
  ): Promise<void> {
534
+ // Pre-compute outputTagIds for this user BEFORE opening cursor.
535
+ // This avoids nested queries inside the cursor loop which cause IDB transaction timeouts.
536
+ let userOutputTagIds: Set<number> | undefined
537
+ if (userId !== undefined) {
538
+ userOutputTagIds = new Set<number>()
539
+ const userTags = await this.findOutputTags({ partial: { userId } })
540
+ for (const tag of userTags) {
541
+ userOutputTagIds.add(tag.outputTagId)
542
+ }
543
+ }
544
+
522
545
  const offset = args.paged?.offset || 0
523
546
  let skipped = 0
524
547
  let count = 0
@@ -550,9 +573,8 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
550
573
  if (args.partial.updated_at && r.updated_at.getTime() !== args.partial.updated_at.getTime()) continue
551
574
  if (args.partial.isDeleted !== undefined && r.isDeleted !== args.partial.isDeleted) continue
552
575
  }
553
- if (userId !== undefined && r.txid) {
554
- const count = await this.countOutputTags({ partial: { userId, outputTagId: r.outputTagId }, trx: args.trx })
555
- if (count === 0) continue
576
+ if (userOutputTagIds !== undefined && !userOutputTagIds.has(r.outputTagId)) {
577
+ continue
556
578
  }
557
579
  if (skipped < offset) {
558
580
  skipped++
@@ -585,6 +607,20 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
585
607
  'args.partial.inputBEEF',
586
608
  `undefined. ProvenTxReqs may not be found by inputBEEF value.`
587
609
  )
610
+
611
+ // Pre-compute txids for this user's transactions BEFORE opening cursor.
612
+ // This avoids nested queries inside the cursor loop which cause IDB transaction timeouts.
613
+ let userTxIds: Set<string> | undefined
614
+ if (userId !== undefined) {
615
+ userTxIds = new Set<string>()
616
+ const userTxs = await this.findTransactions({ partial: { userId }, noRawTx: true })
617
+ for (const tx of userTxs) {
618
+ if (tx.txid) {
619
+ userTxIds.add(tx.txid)
620
+ }
621
+ }
622
+ }
623
+
588
624
  const offset = args.paged?.offset || 0
589
625
  let skipped = 0
590
626
  let count = 0
@@ -629,9 +665,8 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
629
665
  if (args.partial.history && r.history !== args.partial.history) continue
630
666
  if (args.partial.notify && r.notify !== args.partial.notify) continue
631
667
  }
632
- if (userId !== undefined && r.txid) {
633
- const count = await this.countTransactions({ partial: { userId, txid: r.txid }, trx: args.trx })
634
- if (count === 0) continue
668
+ if (userTxIds !== undefined && r.txid && !userTxIds.has(r.txid)) {
669
+ continue
635
670
  }
636
671
  if (skipped < offset) {
637
672
  skipped++
@@ -660,6 +695,20 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
660
695
  'args.partial.merklePath',
661
696
  `undefined. ProvenTxs may not be found by merklePath value.`
662
697
  )
698
+
699
+ // Pre-compute provenTxIds for this user's transactions BEFORE opening cursor.
700
+ // This avoids nested queries inside the cursor loop which cause IDB transaction timeouts.
701
+ let userProvenTxIds: Set<number> | undefined
702
+ if (userId !== undefined) {
703
+ userProvenTxIds = new Set<number>()
704
+ const userTxs = await this.findTransactions({ partial: { userId }, noRawTx: true })
705
+ for (const tx of userTxs) {
706
+ if (tx.provenTxId !== undefined) {
707
+ userProvenTxIds.add(tx.provenTxId)
708
+ }
709
+ }
710
+ }
711
+
663
712
  const offset = args.paged?.offset || 0
664
713
  let skipped = 0
665
714
  let count = 0
@@ -692,9 +741,8 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
692
741
  if (args.partial.blockHash && r.blockHash !== args.partial.blockHash) continue
693
742
  if (args.partial.merkleRoot && r.merkleRoot !== args.partial.merkleRoot) continue
694
743
  }
695
- if (userId !== undefined) {
696
- const count = await this.countTransactions({ partial: { userId, provenTxId: r.provenTxId }, trx: args.trx })
697
- if (count === 0) continue
744
+ if (userProvenTxIds !== undefined && !userProvenTxIds.has(r.provenTxId)) {
745
+ continue
698
746
  }
699
747
  if (skipped < offset) {
700
748
  skipped++
@@ -720,6 +768,17 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
720
768
  filtered: (v: TableTxLabelMap) => void,
721
769
  userId?: number
722
770
  ): Promise<void> {
771
+ // Pre-compute txLabelIds for this user BEFORE opening cursor.
772
+ // This avoids nested queries inside the cursor loop which cause IDB transaction timeouts.
773
+ let userTxLabelIds: Set<number> | undefined
774
+ if (userId !== undefined) {
775
+ userTxLabelIds = new Set<number>()
776
+ const userLabels = await this.findTxLabels({ partial: { userId } })
777
+ for (const label of userLabels) {
778
+ userTxLabelIds.add(label.txLabelId)
779
+ }
780
+ }
781
+
723
782
  const offset = args.paged?.offset || 0
724
783
  let skipped = 0
725
784
  let count = 0
@@ -750,9 +809,8 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
750
809
  if (args.partial.updated_at && r.updated_at.getTime() !== args.partial.updated_at.getTime()) continue
751
810
  if (args.partial.isDeleted !== undefined && r.isDeleted !== args.partial.isDeleted) continue
752
811
  }
753
- if (userId !== undefined) {
754
- const count = await this.countTxLabels({ partial: { userId, txLabelId: r.txLabelId }, trx: args.trx })
755
- if (count === 0) continue
812
+ if (userTxLabelIds !== undefined && !userTxLabelIds.has(r.txLabelId)) {
813
+ continue
756
814
  }
757
815
  if (skipped < offset) {
758
816
  skipped++
@@ -1014,7 +1072,16 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
1014
1072
  }
1015
1073
  const u = this.validatePartialForUpdate(update)
1016
1074
  const dbTrx = this.toDbTrx([storeName], 'readwrite', trx)
1017
- const store = dbTrx.objectStore(storeName)
1075
+ let store: any
1076
+ try {
1077
+ store = dbTrx.objectStore(storeName)
1078
+ } catch (e) {
1079
+ console.error(
1080
+ `[StorageIdb.updateIdb] objectStore('${storeName}') failed for id=${JSON.stringify(id)}, trx=${!!trx}:`,
1081
+ e
1082
+ )
1083
+ throw e
1084
+ }
1018
1085
  const ids = Array.isArray(id) ? id : [id]
1019
1086
  try {
1020
1087
  for (const i of ids) {
@@ -1189,7 +1256,13 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
1189
1256
  await tx.done
1190
1257
  return r
1191
1258
  } catch (err) {
1192
- tx.abort()
1259
+ // Log more detail about transaction errors to help debug IDB issues
1260
+ console.error('[StorageIdb] Transaction error:', err instanceof Error ? err.message : err)
1261
+ try {
1262
+ tx.abort()
1263
+ } catch (abortErr) {
1264
+ console.error('[StorageIdb] Error aborting transaction:', abortErr)
1265
+ }
1193
1266
  await tx.done
1194
1267
  throw err
1195
1268
  }
@@ -1649,7 +1722,9 @@ export class StorageIdb extends StorageProvider implements WalletStorageProvider
1649
1722
  )
1650
1723
  for (const o of results) {
1651
1724
  if (!args.noScript) {
1652
- await this.validateOutputScript(o)
1725
+ // Pass the transaction to avoid creating separate IDB operations
1726
+ // that would cause a passed transaction to auto-commit
1727
+ await this.validateOutputScript(o, args.trx)
1653
1728
  } else {
1654
1729
  o.lockingScript = undefined
1655
1730
  }
@@ -229,25 +229,41 @@ async function createNewInputs(
229
229
  const o = i.output
230
230
  newInputs.push({ i, o })
231
231
  if (o) {
232
+ // IndexedDB transactions auto-commit when there are no pending IDB operations.
233
+ // Network calls (like getBeefForTransaction) create async gaps that cause the
234
+ // transaction to commit prematurely, making subsequent IDB operations fail.
235
+ // We do an initial read + potential network calls outside the transaction,
236
+ // then re-read inside the transaction to verify state hasn't changed.
237
+ const o2 = verifyOne(await storage.findOutputs({ partial: { outputId: o.outputId } }))
238
+ let competingBeef: number[] | undefined
239
+ if (o2.spentBy !== undefined) {
240
+ const spendingTx = await storage.findTransactionById(verifyId(o2.spentBy))
241
+ if (spendingTx && spendingTx.txid) {
242
+ // Fetch beef outside transaction (may involve network calls)
243
+ const beef = await storage.getBeefForTransaction(spendingTx.txid, {})
244
+ competingBeef = beef.toBinary()
245
+ }
246
+ }
247
+
248
+ // Transaction contains only IDB operations: re-read to verify state, then write
232
249
  await storage.transaction(async trx => {
233
- const o2 = verifyOne(await storage.findOutputs({ partial: { outputId: o.outputId }, trx }))
234
- if (o2.spentBy !== undefined) {
235
- const spendingTx = await storage.findTransactionById(verifyId(o2.spentBy), trx)
250
+ const o3 = verifyOne(await storage.findOutputs({ partial: { outputId: o.outputId }, trx }))
251
+ if (o3.spentBy !== undefined) {
252
+ const spendingTx = await storage.findTransactionById(verifyId(o3.spentBy), trx)
236
253
  if (spendingTx && spendingTx.txid) {
237
- const beef = await storage.getBeefForTransaction(spendingTx.txid, {})
238
254
  const rar: ReviewActionResult = {
239
255
  txid: '',
240
256
  status: 'doubleSpend',
241
257
  competingTxs: [spendingTx.txid!],
242
- competingBeef: beef.toBinary()
258
+ competingBeef: competingBeef
243
259
  }
244
260
  throw new WERR_REVIEW_ACTIONS([rar], [])
245
261
  }
246
262
  }
247
- if (o2.spendable != true) {
263
+ if (o3.spendable != true) {
248
264
  throw new WERR_INVALID_PARAMETER(
249
265
  `inputs[${i.vin}]`,
250
- `spendable output. output ${o.txid}:${o.vout} appears to have been spent (spendable=${o2.spendable}).`
266
+ `spendable output. output ${o.txid}:${o.vout} appears to have been spent (spendable=${o3.spendable}).`
251
267
  )
252
268
  }
253
269
  await storage.updateOutput(
@@ -1,14 +0,0 @@
1
- {
2
- "permissions": {
3
- "allow": [
4
- "Bash(cat:*)",
5
- "Bash(git -C /Users/satchmo/wallet-toolbox log --oneline -20 --all -- mobile/package.json)",
6
- "Bash(git -C /Users/satchmo/wallet-toolbox log --oneline -5 --all -- \"*.npmignore\")",
7
- "Bash(git -C /Users/satchmo/wallet-toolbox log --oneline)",
8
- "Bash(npm run build:*)",
9
- "Bash(git add:*)",
10
- "Bash(git commit:*)",
11
- "Bash(git push:*)"
12
- ]
13
- }
14
- }