@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/SECURITY-ISSUE-WPM-TOKEN-THEFT.md +218 -0
- package/WPM-vulnerability.md +23 -0
- package/out/src/sdk/validationHelpers.d.ts +303 -0
- package/out/src/sdk/validationHelpers.d.ts.map +1 -0
- package/out/src/sdk/validationHelpers.js +632 -0
- package/out/src/sdk/validationHelpers.js.map +1 -0
- package/out/src/storage/StorageIdb.d.ts.map +1 -1
- package/out/src/storage/StorageIdb.js +83 -20
- package/out/src/storage/StorageIdb.js.map +1 -1
- package/out/src/storage/methods/createAction.js +22 -7
- package/out/src/storage/methods/createAction.js.map +1 -1
- package/out/tsconfig.all.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/storage/StorageIdb.ts +91 -16
- package/src/storage/methods/createAction.ts +23 -7
- package/.claude/settings.local.json +0 -14
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bopen-io/wallet-toolbox",
|
|
3
|
-
"version": "1.7.
|
|
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.
|
|
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
|
-
|
|
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 (
|
|
554
|
-
|
|
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 (
|
|
633
|
-
|
|
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 (
|
|
696
|
-
|
|
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 (
|
|
754
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
234
|
-
if (
|
|
235
|
-
const spendingTx = await storage.findTransactionById(verifyId(
|
|
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:
|
|
258
|
+
competingBeef: competingBeef
|
|
243
259
|
}
|
|
244
260
|
throw new WERR_REVIEW_ACTIONS([rar], [])
|
|
245
261
|
}
|
|
246
262
|
}
|
|
247
|
-
if (
|
|
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=${
|
|
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
|
-
}
|