@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.
- package/dist/cjs/package.json +2 -2
- package/dist/cjs/src/Engine.js +215 -172
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/storage/knex/all-migrations.js +3 -1
- package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.js +20 -0
- package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/Engine.js +215 -171
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/storage/knex/all-migrations.js +3 -1
- package/dist/esm/src/storage/knex/all-migrations.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.js +16 -0
- package/dist/esm/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.d.ts +9 -0
- package/dist/types/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.d.ts.map +1 -0
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/docs/API.md +25 -2
- package/package.json +2 -2
- package/src/Engine.ts +248 -188
- package/src/storage/knex/all-migrations.ts +3 -3
- package/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.ts +18 -0
- package/dist/cjs/src/AdmittanceInstructions.js +0 -3
- package/dist/cjs/src/AdmittanceInstructions.js.map +0 -1
- package/dist/cjs/src/LookupAnswer.js +0 -3
- package/dist/cjs/src/LookupAnswer.js.map +0 -1
- package/dist/cjs/src/LookupQuestion.js +0 -3
- package/dist/cjs/src/LookupQuestion.js.map +0 -1
- package/dist/cjs/src/SHIPAdvertisement.js +0 -3
- package/dist/cjs/src/SHIPAdvertisement.js.map +0 -1
- package/dist/cjs/src/SLAPAdvertisement.js +0 -3
- package/dist/cjs/src/SLAPAdvertisement.js.map +0 -1
- package/dist/cjs/src/STEAK.js +0 -3
- package/dist/cjs/src/STEAK.js.map +0 -1
- package/dist/cjs/src/TaggedBEEF.js +0 -3
- package/dist/cjs/src/TaggedBEEF.js.map +0 -1
- package/dist/esm/src/AdmittanceInstructions.js +0 -2
- package/dist/esm/src/AdmittanceInstructions.js.map +0 -1
- package/dist/esm/src/LookupAnswer.js +0 -2
- package/dist/esm/src/LookupAnswer.js.map +0 -1
- package/dist/esm/src/LookupQuestion.js +0 -2
- package/dist/esm/src/LookupQuestion.js.map +0 -1
- package/dist/esm/src/SHIPAdvertisement.js +0 -2
- package/dist/esm/src/SHIPAdvertisement.js.map +0 -1
- package/dist/esm/src/SLAPAdvertisement.js +0 -2
- package/dist/esm/src/SLAPAdvertisement.js.map +0 -1
- package/dist/esm/src/STEAK.js +0 -2
- package/dist/esm/src/STEAK.js.map +0 -1
- package/dist/esm/src/TaggedBEEF.js +0 -2
- package/dist/esm/src/TaggedBEEF.js.map +0 -1
- package/dist/types/src/AdmittanceInstructions.d.ts +0 -14
- package/dist/types/src/AdmittanceInstructions.d.ts.map +0 -1
- package/dist/types/src/LookupAnswer.d.ts +0 -15
- package/dist/types/src/LookupAnswer.d.ts.map +0 -1
- package/dist/types/src/LookupQuestion.d.ts +0 -15
- package/dist/types/src/LookupQuestion.d.ts.map +0 -1
- package/dist/types/src/SHIPAdvertisement.d.ts +0 -9
- package/dist/types/src/SHIPAdvertisement.d.ts.map +0 -1
- package/dist/types/src/SLAPAdvertisement.d.ts +0 -9
- package/dist/types/src/SLAPAdvertisement.d.ts.map +0 -1
- package/dist/types/src/STEAK.d.ts +0 -10
- package/dist/types/src/STEAK.d.ts.map +0 -1
- package/dist/types/src/TaggedBEEF.d.ts +0 -11
- 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
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
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
|
-
|
|
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
|
-
|
|
188
|
-
|
|
189
|
-
|
|
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
|
|
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 (
|
|
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
|
-
|
|
257
|
-
|
|
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
|
-
//
|
|
277
|
-
|
|
278
|
-
|
|
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
|
-
|
|
299
|
-
|
|
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
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
outputIndex,
|
|
399
|
-
topic,
|
|
400
|
-
offChainValues
|
|
389
|
+
outputsConsumed.push({
|
|
390
|
+
txid: previousTXID,
|
|
391
|
+
outputIndex: previousOutputIndex
|
|
401
392
|
})
|
|
402
393
|
}
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
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.
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"AdmittanceInstructions.js","sourceRoot":"","sources":["../../../src/AdmittanceInstructions.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"LookupAnswer.js","sourceRoot":"","sources":["../../../src/LookupAnswer.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"LookupQuestion.js","sourceRoot":"","sources":["../../../src/LookupQuestion.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SHIPAdvertisement.js","sourceRoot":"","sources":["../../../src/SHIPAdvertisement.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SLAPAdvertisement.js","sourceRoot":"","sources":["../../../src/SLAPAdvertisement.ts"],"names":[],"mappings":""}
|
package/dist/cjs/src/STEAK.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"STEAK.js","sourceRoot":"","sources":["../../../src/STEAK.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"TaggedBEEF.js","sourceRoot":"","sources":["../../../src/TaggedBEEF.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"AdmittanceInstructions.js","sourceRoot":"","sources":["../../../src/AdmittanceInstructions.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"LookupAnswer.js","sourceRoot":"","sources":["../../../src/LookupAnswer.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"LookupQuestion.js","sourceRoot":"","sources":["../../../src/LookupQuestion.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SHIPAdvertisement.js","sourceRoot":"","sources":["../../../src/SHIPAdvertisement.ts"],"names":[],"mappings":""}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"SLAPAdvertisement.js","sourceRoot":"","sources":["../../../src/SLAPAdvertisement.ts"],"names":[],"mappings":""}
|
package/dist/esm/src/STEAK.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"STEAK.js","sourceRoot":"","sources":["../../../src/STEAK.ts"],"names":[],"mappings":""}
|
|
@@ -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
|