@bsv/overlay 0.5.1 → 0.5.3

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 (67) hide show
  1. package/dist/cjs/package.json +2 -2
  2. package/dist/cjs/src/Engine.js +215 -172
  3. package/dist/cjs/src/Engine.js.map +1 -1
  4. package/dist/cjs/src/storage/knex/all-migrations.js +3 -1
  5. package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
  6. package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.js +20 -0
  7. package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.js.map +1 -0
  8. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  9. package/dist/esm/src/Engine.js +215 -171
  10. package/dist/esm/src/Engine.js.map +1 -1
  11. package/dist/esm/src/storage/knex/all-migrations.js +3 -1
  12. package/dist/esm/src/storage/knex/all-migrations.js.map +1 -1
  13. package/dist/esm/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.js +16 -0
  14. package/dist/esm/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.js.map +1 -0
  15. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  16. package/dist/types/src/Engine.d.ts.map +1 -1
  17. package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
  18. package/dist/types/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.d.ts +9 -0
  19. package/dist/types/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.d.ts.map +1 -0
  20. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  21. package/docs/API.md +25 -2
  22. package/package.json +2 -2
  23. package/src/Engine.ts +248 -188
  24. package/src/storage/knex/all-migrations.ts +3 -3
  25. package/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.ts +18 -0
  26. package/dist/cjs/src/AdmittanceInstructions.js +0 -3
  27. package/dist/cjs/src/AdmittanceInstructions.js.map +0 -1
  28. package/dist/cjs/src/LookupAnswer.js +0 -3
  29. package/dist/cjs/src/LookupAnswer.js.map +0 -1
  30. package/dist/cjs/src/LookupQuestion.js +0 -3
  31. package/dist/cjs/src/LookupQuestion.js.map +0 -1
  32. package/dist/cjs/src/SHIPAdvertisement.js +0 -3
  33. package/dist/cjs/src/SHIPAdvertisement.js.map +0 -1
  34. package/dist/cjs/src/SLAPAdvertisement.js +0 -3
  35. package/dist/cjs/src/SLAPAdvertisement.js.map +0 -1
  36. package/dist/cjs/src/STEAK.js +0 -3
  37. package/dist/cjs/src/STEAK.js.map +0 -1
  38. package/dist/cjs/src/TaggedBEEF.js +0 -3
  39. package/dist/cjs/src/TaggedBEEF.js.map +0 -1
  40. package/dist/esm/src/AdmittanceInstructions.js +0 -2
  41. package/dist/esm/src/AdmittanceInstructions.js.map +0 -1
  42. package/dist/esm/src/LookupAnswer.js +0 -2
  43. package/dist/esm/src/LookupAnswer.js.map +0 -1
  44. package/dist/esm/src/LookupQuestion.js +0 -2
  45. package/dist/esm/src/LookupQuestion.js.map +0 -1
  46. package/dist/esm/src/SHIPAdvertisement.js +0 -2
  47. package/dist/esm/src/SHIPAdvertisement.js.map +0 -1
  48. package/dist/esm/src/SLAPAdvertisement.js +0 -2
  49. package/dist/esm/src/SLAPAdvertisement.js.map +0 -1
  50. package/dist/esm/src/STEAK.js +0 -2
  51. package/dist/esm/src/STEAK.js.map +0 -1
  52. package/dist/esm/src/TaggedBEEF.js +0 -2
  53. package/dist/esm/src/TaggedBEEF.js.map +0 -1
  54. package/dist/types/src/AdmittanceInstructions.d.ts +0 -14
  55. package/dist/types/src/AdmittanceInstructions.d.ts.map +0 -1
  56. package/dist/types/src/LookupAnswer.d.ts +0 -15
  57. package/dist/types/src/LookupAnswer.d.ts.map +0 -1
  58. package/dist/types/src/LookupQuestion.d.ts +0 -15
  59. package/dist/types/src/LookupQuestion.d.ts.map +0 -1
  60. package/dist/types/src/SHIPAdvertisement.d.ts +0 -9
  61. package/dist/types/src/SHIPAdvertisement.d.ts.map +0 -1
  62. package/dist/types/src/SLAPAdvertisement.d.ts +0 -9
  63. package/dist/types/src/SLAPAdvertisement.d.ts.map +0 -1
  64. package/dist/types/src/STEAK.d.ts +0 -10
  65. package/dist/types/src/STEAK.d.ts.map +0 -1
  66. package/dist/types/src/TaggedBEEF.d.ts +0 -11
  67. package/dist/types/src/TaggedBEEF.d.ts.map +0 -1
package/src/Engine.ts CHANGED
@@ -151,44 +151,136 @@ export class Engine {
151
151
 
152
152
  const steak: STEAK = {}
153
153
  const dupeTopics = new Set<string>()
154
+ const failedTopics = new Set<string>()
155
+
156
+ // ===================================================================
157
+ // PHASE 1: VALIDATE (read-only, no mutations)
158
+ // ===================================================================
159
+ type TopicValidation = {
160
+ topic: string
161
+ isDupe: boolean
162
+ previousCoins: number[]
163
+ previousOutputs: Array<Output | null>
164
+ admissibleOutputs: AdmittanceInstructions
165
+ }
154
166
 
155
- // Parallelize the topic processing
156
- const topicPromises = taggedBEEF.topics.map(async topic => {
157
- if (this.managers[topic] === undefined || this.managers[topic] === null) {
158
- throw new Error(`This server does not support this topic: ${topic}`)
159
- }
167
+ const topicValidations = taggedBEEF.topics.map(async (topic): Promise<TopicValidation> => {
168
+ try {
169
+ if (this.managers[topic] === undefined || this.managers[topic] === null) {
170
+ throw new Error(`This server does not support this topic: ${topic}`)
171
+ }
172
+
173
+ // Check for duplicate transactions
174
+ this.startTime(`dupCheck_${txid.substring(0, 10)}`)
175
+ const dupeCheck = await this.storage.doesAppliedTransactionExist({ txid, topic })
176
+ this.endTime(`dupCheck_${txid.substring(0, 10)}`)
177
+
178
+ if (dupeCheck) {
179
+ dupeTopics.add(topic)
180
+ return {
181
+ topic,
182
+ isDupe: true,
183
+ previousCoins: [],
184
+ previousOutputs: [],
185
+ admissibleOutputs: { outputsToAdmit: [], coinsToRetain: [] }
186
+ }
187
+ }
160
188
 
161
- // Check for duplicate transactions
162
- this.startTime(`dupCheck_${txid.substring(0, 10)}`)
163
- const dupeCheck = await this.storage.doesAppliedTransactionExist({ txid, topic })
164
- this.endTime(`dupCheck_${txid.substring(0, 10)}`)
189
+ // Identify previous coins admitted to this specific topic
190
+ const previousCoins: number[] = []
191
+ const outputPromises = tx.inputs.map(async (input, i) => {
192
+ const previousTXID = input.sourceTXID !== undefined ? input.sourceTXID : input.sourceTransaction?.id('hex')
193
+ if (previousTXID !== undefined) {
194
+ // Check if the previous output was admitted to this specific topic
195
+ const output = await this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic)
196
+ if (output !== undefined && output !== null) {
197
+ previousCoins.push(i)
198
+ return output
199
+ }
200
+ }
201
+ return null
202
+ })
165
203
 
166
- if (dupeCheck) {
167
- dupeTopics.add(topic)
168
- steak[topic] = { outputsToAdmit: [], coinsToRetain: [] }
169
- return
204
+ this.startTime(`previousOutputQuery_${txid.substring(0, 10)}`)
205
+ const previousOutputs = await Promise.all(outputPromises)
206
+ this.endTime(`previousOutputQuery_${txid.substring(0, 10)}`)
207
+
208
+ // Determine which outputs are admissible for this topic (validation only)
209
+ this.startTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`)
210
+ const admissibleOutputs = await this.managers[topic].identifyAdmissibleOutputs(
211
+ taggedBEEF.beef,
212
+ previousCoins,
213
+ offChainValues
214
+ )
215
+ this.endTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`)
216
+
217
+ return {
218
+ topic,
219
+ isDupe: false,
220
+ previousCoins,
221
+ previousOutputs,
222
+ admissibleOutputs
223
+ }
224
+ } catch (error) {
225
+ this.logger.error('Error validating topic during submit:', error)
226
+ failedTopics.add(topic)
227
+ return {
228
+ topic,
229
+ isDupe: false,
230
+ previousCoins: [],
231
+ previousOutputs: [],
232
+ admissibleOutputs: { outputsToAdmit: [], coinsToRetain: [] }
233
+ }
170
234
  }
235
+ })
171
236
 
172
- // Identify previous coins admitted to this specific topic
173
- const previousCoins: number[] = []
174
- const outputPromises = tx.inputs.map(async (input, i) => {
175
- const previousTXID = input.sourceTXID !== undefined ? input.sourceTXID : input.sourceTransaction?.id('hex')
176
- if (previousTXID !== undefined) {
177
- // Check if the previous output was admitted to this specific topic
178
- const output = await this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic)
179
- if (output !== undefined && output !== null) {
180
- previousCoins.push(i)
181
- return output
182
- }
237
+ const validations = await Promise.all(topicValidations)
238
+
239
+ // Build preliminary STEAK from validation results
240
+ for (const validation of validations) {
241
+ steak[validation.topic] = validation.admissibleOutputs
242
+ }
243
+
244
+ // ===================================================================
245
+ // PHASE 2: BROADCAST (before any mutations)
246
+ // ===================================================================
247
+ this.startTime(`broadcast_${txid.substring(0, 10)}`)
248
+ if (mode !== 'historical-tx' && this.broadcaster !== undefined) {
249
+ try {
250
+ const response = await this.broadcaster.broadcast(tx)
251
+ if (isBroadcastFailure(response) && this.throwOnBroadcastFailure) {
252
+ const e = new Error(`Failed to broadcast transaction! Error: ${response.description}`)
253
+ ; (e as any).more = response.more
254
+ throw e
183
255
  }
184
- return null
185
- })
256
+ } catch (error) {
257
+ if (this.throwOnBroadcastFailure) {
258
+ throw error
259
+ }
260
+ this.logger.error('Error broadcasting transaction:', error)
261
+ }
262
+ }
263
+ this.endTime(`broadcast_${txid.substring(0, 10)}`)
186
264
 
187
- this.startTime(`previousOutputQuery_${txid.substring(0, 10)}`)
188
- const outputs = await Promise.all(outputPromises)
189
- this.endTime(`previousOutputQuery_${txid.substring(0, 10)}`)
265
+ // Call the callback function with STEAK if it is provided (before storage mutations)
266
+ if (onSteakReady !== undefined) {
267
+ onSteakReady(steak)
268
+ }
269
+
270
+ // ===================================================================
271
+ // PHASE 3: MUTATE STORAGE (only after broadcast succeeded)
272
+ // ===================================================================
273
+ // Mark previous outputs as spent and notify lookup services
274
+ await Promise.all(validations.map(async (validation) => {
275
+ if (validation.isDupe || failedTopics.has(validation.topic)) {
276
+ return
277
+ }
190
278
 
191
- const markSpentPromises = outputs.map(async (output) => {
279
+ const topic = validation.topic
280
+ const previousOutputs = validation.previousOutputs
281
+
282
+ // Mark all previous outputs as spent
283
+ const markSpentPromises = previousOutputs.map(async (output) => {
192
284
  if (output !== undefined && output !== null) {
193
285
  try {
194
286
  await this.storage.markUTXOAsSpent(output.txid, output.outputIndex, topic)
@@ -211,7 +303,7 @@ export class Engine {
211
303
  }
212
304
  return realSource === output.txid && i.sourceOutputIndex === output.outputIndex
213
305
  })
214
- if (!inputIndex) {
306
+ if (inputIndex === -1) {
215
307
  throw new Error('Could not find input index')
216
308
  }
217
309
  await l.outputSpent({
@@ -253,173 +345,141 @@ export class Engine {
253
345
  }
254
346
  })
255
347
 
256
- let admissibleOutputs: AdmittanceInstructions = { outputsToAdmit: [], coinsToRetain: [] }
257
- // Determine which outputs are admissible for this topic
258
- const admissibleOutputsPromise = (async () => {
259
- try {
260
- this.startTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`)
261
- admissibleOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins, offChainValues)
262
- this.endTime(`identifyAdmissibleOutputs_${txid.substring(0, 10)}`)
263
- } catch (_) {
264
- steak[topic] = { outputsToAdmit: [], coinsToRetain: [] }
265
- }
266
- })()
267
-
268
- // Wait for both tasks to complete
269
- await Promise.all([markSpentPromises, admissibleOutputsPromise])
270
- // Keep track of what outputs were admitted for what topic
271
- steak[topic] = admissibleOutputs
272
- })
273
-
274
- await Promise.all(topicPromises)
348
+ await Promise.all(markSpentPromises)
349
+ }))
275
350
 
276
- // Broadcast the transaction if not historical and broadcaster is configured
277
- this.startTime(`broadcast_${txid.substring(0, 10)}`)
278
- if (mode !== 'historical-tx' && this.broadcaster !== undefined) {
279
- const response = await this.broadcaster.broadcast(tx)
280
- if (isBroadcastFailure(response) && this.throwOnBroadcastFailure) {
281
- const e = new Error(`Failed to broadcast transaction! Error: ${response.description}`);
282
- (e as any).more = response.more
283
- throw e
284
- }
285
- }
286
- this.endTime(`broadcast_${txid.substring(0, 10)}`)
287
-
288
- // Call the callback function if it is provided (moved here to ensure topic processing is complete)
289
- if (onSteakReady !== undefined) {
290
- onSteakReady(steak)
291
- }
292
-
293
- // Update storage and notify lookup services
294
- for (const topic of taggedBEEF.topics) {
351
+ // Continue with storage updates and lookup service notifications
352
+ for (const validation of validations) {
353
+ const topic = validation.topic
295
354
  if (dupeTopics.has(topic)) {
296
355
  continue
297
356
  }
298
- // Keep track of which outputs to admit, mark as stale, or retain
299
- const admissibleOutputs = steak[topic]
300
- const outputsToAdmit: number[] = admissibleOutputs.outputsToAdmit
301
- const outputsConsumed: Array<{
302
- txid: string
303
- outputIndex: number
304
- }> = []
305
-
306
- const outputsToMarkStale: Array<{
307
- txid: string
308
- previousOutputIndex: number
309
- inputIndex: number
310
- }> = []
311
-
312
- // Recompute previousCoins for this topic to use in the update logic
313
- const previousCoins: number[] = []
314
- await Promise.all(tx.inputs.map(async (input, i) => {
315
- const previousTXID = input.sourceTXID !== undefined ? input.sourceTXID : input.sourceTransaction?.id('hex')
316
- if (previousTXID !== undefined) {
317
- const output = await this.storage.findOutput(previousTXID, input.sourceOutputIndex, topic)
318
- if (output !== undefined && output !== null) {
319
- previousCoins.push(i)
320
- }
321
- }
322
- }))
323
-
324
- // For each of the previous UTXOs for this topic, if the UTXO was not included in the list of UTXOs identified for retention, then it will be marked as stale.
325
- for (const inputIndex of previousCoins) {
326
- const previousTXID = tx.inputs[inputIndex].sourceTXID ?? tx.inputs[inputIndex].sourceTransaction?.id('hex')
327
- if (typeof previousTXID !== 'string') continue
328
- const previousOutputIndex = tx.inputs[inputIndex].sourceOutputIndex
329
- if (!admissibleOutputs.coinsToRetain.includes(inputIndex)) {
330
- outputsToMarkStale.push({
331
- txid: previousTXID,
332
- previousOutputIndex,
333
- inputIndex
334
- })
335
- } else {
336
- outputsConsumed.push({
337
- txid: previousTXID,
338
- outputIndex: previousOutputIndex
339
- })
340
- }
357
+ if (failedTopics.has(topic)) {
358
+ continue
341
359
  }
342
-
343
- // Remove stale outputs recursively
344
- this.startTime(`lookForStaleOutputs_${txid.substring(0, 10)}`)
345
- await Promise.all(outputsToMarkStale.map(async coin => {
346
- const output = await this.storage.findOutput(coin.txid, coin.previousOutputIndex, topic)
347
- if (output !== undefined && output !== null) {
348
- await this.deleteUTXODeep(output)
349
- }
350
- }))
351
- this.endTime(`lookForStaleOutputs_${txid.substring(0, 10)}`)
352
-
353
- // Update the STEAK to indicate which coins were removed
354
- steak[topic].coinsRemoved = outputsToMarkStale.map(x => x.inputIndex)
355
-
356
- // Handle admittance and notification of incoming UTXOs
357
- const newUTXOs: Array<{ txid: string, outputIndex: number }> = []
358
- await Promise.all(outputsToAdmit.map(async outputIndex => {
359
- if (typeof tx.outputs[outputIndex].satoshis !== 'number') return
360
- this.startTime(`insertNewOutput_${txid.substring(0, 10)}`)
361
- await this.storage.insertOutput({
362
- txid,
363
- outputIndex,
364
- outputScript: tx.outputs[outputIndex].lockingScript.toBinary(),
365
- satoshis: tx.outputs[outputIndex].satoshis,
366
- topic,
367
- spent: false,
368
- beef: taggedBEEF.beef,
369
- consumedBy: [],
370
- outputsConsumed,
371
- score: Date.now()
372
- })
373
- this.endTime(`insertNewOutput_${txid.substring(0, 10)}`)
374
- newUTXOs.push({ txid, outputIndex })
375
-
376
- this.startTime(`notifyLookupService${txid.substring(0, 10)}`)
377
- await Promise.all(Object.values(this.lookupServices).map(async l => {
378
- if (l.admissionMode === 'locking-script') {
379
- if (
380
- typeof tx.outputs[outputIndex].lockingScript !== 'object' ||
381
- typeof tx.outputs[outputIndex].satoshis !== 'number'
382
- ) {
383
- return
384
- }
385
- await l.outputAdmittedByTopic({
386
- mode: 'locking-script',
387
- txid,
388
- outputIndex,
389
- lockingScript: tx.outputs[outputIndex].lockingScript,
390
- satoshis: tx.outputs[outputIndex].satoshis,
391
- topic,
392
- offChainValues
360
+ try {
361
+ const admissibleOutputs = steak[topic]
362
+ const outputsToAdmit: number[] = admissibleOutputs.outputsToAdmit
363
+ const outputsConsumed: Array<{
364
+ txid: string
365
+ outputIndex: number
366
+ }> = []
367
+
368
+ const outputsToMarkStale: Array<{
369
+ txid: string
370
+ previousOutputIndex: number
371
+ inputIndex: number
372
+ }> = []
373
+
374
+ // Use previousCoins from validation
375
+ const previousCoins = validation.previousCoins
376
+
377
+ // For each of the previous UTXOs for this topic, if the UTXO was not included in the list of UTXOs identified for retention, then it will be marked as stale.
378
+ for (const inputIndex of previousCoins) {
379
+ const previousTXID = tx.inputs[inputIndex].sourceTXID ?? tx.inputs[inputIndex].sourceTransaction?.id('hex')
380
+ if (typeof previousTXID !== 'string') continue
381
+ const previousOutputIndex = tx.inputs[inputIndex].sourceOutputIndex
382
+ if (!admissibleOutputs.coinsToRetain.includes(inputIndex)) {
383
+ outputsToMarkStale.push({
384
+ txid: previousTXID,
385
+ previousOutputIndex,
386
+ inputIndex
393
387
  })
394
388
  } else {
395
- await l.outputAdmittedByTopic({
396
- mode: 'whole-tx',
397
- atomicBEEF: tx.toAtomicBEEF(),
398
- outputIndex,
399
- topic,
400
- offChainValues
389
+ outputsConsumed.push({
390
+ txid: previousTXID,
391
+ outputIndex: previousOutputIndex
401
392
  })
402
393
  }
403
- }))
404
- this.endTime(`notifyLookupService${txid.substring(0, 10)}`)
405
- }))
406
-
407
- this.startTime(`outputConsumed_${txid.substring(0, 10)}`)
408
- // Update each output consumed to know who consumed it and insert applied transaction in parallel
409
- await Promise.all([
410
- ...outputsConsumed.map(async output => {
411
- const outputToUpdate = await this.storage.findOutput(output.txid, output.outputIndex, topic)
412
- if (outputToUpdate !== undefined && outputToUpdate !== null) {
413
- const newConsumedBy = [...new Set([...newUTXOs, ...outputToUpdate.consumedBy])]
414
- await this.storage.updateConsumedBy(output.txid, output.outputIndex, topic, newConsumedBy)
394
+ }
395
+
396
+ // Remove stale outputs recursively
397
+ this.startTime(`lookForStaleOutputs_${txid.substring(0, 10)}`)
398
+ await Promise.all(outputsToMarkStale.map(async coin => {
399
+ const output = await this.storage.findOutput(coin.txid, coin.previousOutputIndex, topic)
400
+ if (output !== undefined && output !== null) {
401
+ await this.deleteUTXODeep(output)
415
402
  }
416
- }),
417
- this.storage.insertAppliedTransaction({
418
- txid,
419
- topic
420
- })
421
- ])
422
- this.endTime(`outputConsumed_${txid.substring(0, 10)}`)
403
+ }))
404
+ this.endTime(`lookForStaleOutputs_${txid.substring(0, 10)}`)
405
+
406
+ // Update the STEAK to indicate which coins were removed
407
+ steak[topic].coinsRemoved = outputsToMarkStale.map(x => x.inputIndex)
408
+
409
+ // Handle admittance and notification of incoming UTXOs
410
+ const newUTXOs: Array<{ txid: string, outputIndex: number }> = []
411
+ await Promise.all(outputsToAdmit.map(async outputIndex => {
412
+ if (typeof tx.outputs[outputIndex].satoshis !== 'number') return
413
+ this.startTime(`insertNewOutput_${txid.substring(0, 10)}`)
414
+ await this.storage.insertOutput({
415
+ txid,
416
+ outputIndex,
417
+ outputScript: tx.outputs[outputIndex].lockingScript.toBinary(),
418
+ satoshis: tx.outputs[outputIndex].satoshis,
419
+ topic,
420
+ spent: false,
421
+ beef: taggedBEEF.beef,
422
+ consumedBy: [],
423
+ outputsConsumed,
424
+ score: Date.now()
425
+ })
426
+ this.endTime(`insertNewOutput_${txid.substring(0, 10)}`)
427
+ newUTXOs.push({ txid, outputIndex })
428
+
429
+ this.startTime(`notifyLookupService${txid.substring(0, 10)}`)
430
+ await Promise.all(Object.values(this.lookupServices).map(async l => {
431
+ try {
432
+ if (l.admissionMode === 'locking-script') {
433
+ if (
434
+ typeof tx.outputs[outputIndex].lockingScript !== 'object' ||
435
+ typeof tx.outputs[outputIndex].satoshis !== 'number'
436
+ ) {
437
+ return
438
+ }
439
+ await l.outputAdmittedByTopic({
440
+ mode: 'locking-script',
441
+ txid,
442
+ outputIndex,
443
+ lockingScript: tx.outputs[outputIndex].lockingScript,
444
+ satoshis: tx.outputs[outputIndex].satoshis,
445
+ topic,
446
+ offChainValues
447
+ })
448
+ } else {
449
+ await l.outputAdmittedByTopic({
450
+ mode: 'whole-tx',
451
+ atomicBEEF: tx.toAtomicBEEF(),
452
+ outputIndex,
453
+ topic,
454
+ offChainValues
455
+ })
456
+ }
457
+ } catch (error) {
458
+ this.logger.error('Error in lookup service for outputAdmittedByTopic:', error)
459
+ }
460
+ }))
461
+ this.endTime(`notifyLookupService${txid.substring(0, 10)}`)
462
+ }))
463
+
464
+ this.startTime(`outputConsumed_${txid.substring(0, 10)}`)
465
+ // Update each output consumed to know who consumed it and insert applied transaction in parallel
466
+ await Promise.all([
467
+ ...outputsConsumed.map(async output => {
468
+ const outputToUpdate = await this.storage.findOutput(output.txid, output.outputIndex, topic)
469
+ if (outputToUpdate !== undefined && outputToUpdate !== null) {
470
+ const newConsumedBy = [...new Set([...newUTXOs, ...outputToUpdate.consumedBy])]
471
+ await this.storage.updateConsumedBy(output.txid, output.outputIndex, topic, newConsumedBy)
472
+ }
473
+ }),
474
+ this.storage.insertAppliedTransaction({
475
+ txid,
476
+ topic
477
+ })
478
+ ])
479
+ this.endTime(`outputConsumed_${txid.substring(0, 10)}`)
480
+ } catch (error) {
481
+ this.logger.error('Error updating storage and notifying lookup services for topic', topic, error)
482
+ }
423
483
  }
424
484
 
425
485
  // If we don't have an advertiser or we are dealing with historical transactions, just return the steak
@@ -6,8 +6,7 @@ import { up as addedIndexesUp, down as addedIndexesDown } from './migrations/202
6
6
  import { up as enlargeUp, down as enlargeDown } from './migrations/2025-05-28-001-enlarge.js'
7
7
  import { up as gaspPaginationSupportUp, down as gaspPaginationSupportDown } from './migrations/2025-06-25-001-gasp-pagination-support.js'
8
8
  import { up as fixScoreColumnTypeUp, down as fixScoreColumnTypeDown } from './migrations/2025-07-22-001-fix-score-column-type.js'
9
-
10
-
9
+ import { up as utxoLookupIndexUp, down as utxoLookupIndexDown } from './migrations/2025-11-11-001-utxo-lookup-index.js'
11
10
 
12
11
  /**
13
12
  * An array of all migrations, in order.
@@ -24,7 +23,8 @@ const allMigrations: Migration[] = [
24
23
  { up: addedIndexesUp, down: addedIndexesDown },
25
24
  { up: enlargeUp, down: enlargeDown },
26
25
  { up: gaspPaginationSupportUp, down: gaspPaginationSupportDown },
27
- { up: fixScoreColumnTypeUp, down: fixScoreColumnTypeDown }
26
+ { up: fixScoreColumnTypeUp, down: fixScoreColumnTypeDown },
27
+ { up: utxoLookupIndexUp, down: utxoLookupIndexDown }
28
28
  ]
29
29
 
30
30
  export default allMigrations
@@ -0,0 +1,18 @@
1
+ import type { Knex } from 'knex'
2
+
3
+ /**
4
+ * Adds optimized index for findUTXOsForTopic queries.
5
+ * This query pattern is: WHERE topic = ? AND spent = false ORDER BY score
6
+ * The composite index (topic, spent, score) enables efficient range scans.
7
+ */
8
+ export async function up (knex: Knex): Promise<void> {
9
+ await knex.schema.table('outputs', function (table) {
10
+ table.index(['topic', 'spent', 'score'], 'idx_outputs_topic_spent_score')
11
+ })
12
+ }
13
+
14
+ export async function down (knex: Knex): Promise<void> {
15
+ await knex.schema.table('outputs', function (table) {
16
+ table.dropIndex(['topic', 'spent', 'score'], 'idx_outputs_topic_spent_score')
17
+ })
18
+ }
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=AdmittanceInstructions.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AdmittanceInstructions.js","sourceRoot":"","sources":["../../../src/AdmittanceInstructions.ts"],"names":[],"mappings":""}
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=LookupAnswer.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"LookupAnswer.js","sourceRoot":"","sources":["../../../src/LookupAnswer.ts"],"names":[],"mappings":""}
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=LookupQuestion.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"LookupQuestion.js","sourceRoot":"","sources":["../../../src/LookupQuestion.ts"],"names":[],"mappings":""}
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=SHIPAdvertisement.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SHIPAdvertisement.js","sourceRoot":"","sources":["../../../src/SHIPAdvertisement.ts"],"names":[],"mappings":""}
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=SLAPAdvertisement.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SLAPAdvertisement.js","sourceRoot":"","sources":["../../../src/SLAPAdvertisement.ts"],"names":[],"mappings":""}
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=STEAK.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"STEAK.js","sourceRoot":"","sources":["../../../src/STEAK.ts"],"names":[],"mappings":""}
@@ -1,3 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- //# sourceMappingURL=TaggedBEEF.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"TaggedBEEF.js","sourceRoot":"","sources":["../../../src/TaggedBEEF.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=AdmittanceInstructions.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"AdmittanceInstructions.js","sourceRoot":"","sources":["../../../src/AdmittanceInstructions.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=LookupAnswer.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"LookupAnswer.js","sourceRoot":"","sources":["../../../src/LookupAnswer.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=LookupQuestion.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"LookupQuestion.js","sourceRoot":"","sources":["../../../src/LookupQuestion.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=SHIPAdvertisement.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SHIPAdvertisement.js","sourceRoot":"","sources":["../../../src/SHIPAdvertisement.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=SLAPAdvertisement.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"SLAPAdvertisement.js","sourceRoot":"","sources":["../../../src/SLAPAdvertisement.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=STEAK.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"STEAK.js","sourceRoot":"","sources":["../../../src/STEAK.ts"],"names":[],"mappings":""}
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=TaggedBEEF.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"TaggedBEEF.js","sourceRoot":"","sources":["../../../src/TaggedBEEF.ts"],"names":[],"mappings":""}
@@ -1,14 +0,0 @@
1
- /**
2
- * Instructs the Overlay Services Engine about which outputs to admit and which previous outputs to retain. Returned by a Topic Manager.
3
- */
4
- export type AdmittanceInstructions = {
5
- /**
6
- * The indices of all admissable outputs into the managed topic from the provided transaction.
7
- */
8
- outputsToAdmit: number[];
9
- /**
10
- * The indices of all inputs from the provided transaction which spend previously-admitted outputs that should be retained for historical record-keeping.
11
- */
12
- coinsToRetain: number[];
13
- };
14
- //# sourceMappingURL=AdmittanceInstructions.d.ts.map