@bsv/overlay 2.0.2 → 2.0.4

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 (82) hide show
  1. package/dist/cjs/mod.js +6 -1
  2. package/dist/cjs/mod.js.map +1 -1
  3. package/dist/cjs/package.json +13 -11
  4. package/dist/cjs/src/BASM.js +92 -0
  5. package/dist/cjs/src/BASM.js.map +1 -0
  6. package/dist/cjs/src/BASMRemote.js +43 -0
  7. package/dist/cjs/src/BASMRemote.js.map +1 -0
  8. package/dist/cjs/src/Engine.js +649 -47
  9. package/dist/cjs/src/Engine.js.map +1 -1
  10. package/dist/cjs/src/GASP/OverlayGASPRemote.js +2 -2
  11. package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
  12. package/dist/cjs/src/GASP/OverlayGASPStorage.js +18 -25
  13. package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
  14. package/dist/cjs/src/storage/knex/KnexStorage.js +329 -12
  15. package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
  16. package/dist/cjs/src/storage/knex/all-migrations.js +3 -1
  17. package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
  18. package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js +2 -24
  19. package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
  20. package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js +114 -0
  21. package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -0
  22. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  23. package/dist/esm/mod.js +1 -0
  24. package/dist/esm/mod.js.map +1 -1
  25. package/dist/esm/src/BASM.js +85 -0
  26. package/dist/esm/src/BASM.js.map +1 -0
  27. package/dist/esm/src/BASMRemote.js +42 -0
  28. package/dist/esm/src/BASMRemote.js.map +1 -0
  29. package/dist/esm/src/Engine.js +637 -46
  30. package/dist/esm/src/Engine.js.map +1 -1
  31. package/dist/esm/src/GASP/OverlayGASPRemote.js +2 -2
  32. package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
  33. package/dist/esm/src/GASP/OverlayGASPStorage.js +18 -25
  34. package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
  35. package/dist/esm/src/storage/knex/KnexStorage.js +323 -12
  36. package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
  37. package/dist/esm/src/storage/knex/all-migrations.js +3 -1
  38. package/dist/esm/src/storage/knex/all-migrations.js.map +1 -1
  39. package/dist/esm/src/storage/knex/migrations/2025-05-28-001-enlarge.js +2 -24
  40. package/dist/esm/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
  41. package/dist/esm/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js +109 -0
  42. package/dist/esm/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -0
  43. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  44. package/dist/types/mod.d.ts +2 -0
  45. package/dist/types/mod.d.ts.map +1 -1
  46. package/dist/types/src/BASM.d.ts +110 -0
  47. package/dist/types/src/BASM.d.ts.map +1 -0
  48. package/dist/types/src/BASMRemote.d.ts +14 -0
  49. package/dist/types/src/BASMRemote.d.ts.map +1 -0
  50. package/dist/types/src/Engine.d.ts +78 -1
  51. package/dist/types/src/Engine.d.ts.map +1 -1
  52. package/dist/types/src/GASP/OverlayGASPStorage.d.ts +1 -1
  53. package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
  54. package/dist/types/src/LookupService.d.ts.map +1 -1
  55. package/dist/types/src/storage/Storage.d.ts +118 -0
  56. package/dist/types/src/storage/Storage.d.ts.map +1 -1
  57. package/dist/types/src/storage/knex/KnexStorage.d.ts +38 -6
  58. package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
  59. package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
  60. package/dist/types/src/storage/knex/migrations/2025-05-28-001-enlarge.d.ts.map +1 -1
  61. package/dist/types/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts +4 -0
  62. package/dist/types/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts.map +1 -0
  63. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  64. package/docs/BRC-136-BASM.md +285 -0
  65. package/docs/README.md +2 -0
  66. package/docs/Synchronization.md +10 -1
  67. package/mod.ts +25 -1
  68. package/package.json +25 -23
  69. package/src/BASM.ts +208 -0
  70. package/src/BASMRemote.ts +54 -0
  71. package/src/Engine.ts +762 -47
  72. package/src/GASP/OverlayGASPRemote.ts +2 -2
  73. package/src/GASP/OverlayGASPStorage.ts +17 -22
  74. package/src/LookupService.ts +2 -1
  75. package/src/__tests/BASM.test.ts +44 -0
  76. package/src/__tests/BASMChain.test.ts +290 -0
  77. package/src/__tests/Engine.test.ts +3 -4
  78. package/src/storage/Storage.ts +127 -0
  79. package/src/storage/knex/KnexStorage.ts +376 -14
  80. package/src/storage/knex/all-migrations.ts +3 -1
  81. package/src/storage/knex/migrations/2025-05-28-001-enlarge.ts +2 -22
  82. package/src/storage/knex/migrations/2026-05-29-001-brc136-basm.ts +123 -0
@@ -1,6 +1,21 @@
1
- import { Storage } from '../Storage.js'
1
+ import {
2
+ Storage,
3
+ type AppliedTransaction,
4
+ type AppliedTransactionProofUpdate,
5
+ type StoredTransactionRecord,
6
+ type UnprovenAppliedTransactionCandidate
7
+ } from '../Storage.js'
2
8
  import { Knex } from 'knex'
3
9
  import type { Output } from '../../Output.js'
10
+ import { Transaction } from '@bsv/sdk'
11
+ import {
12
+ BASM_ZERO_HASH,
13
+ type AdmittedTxRef,
14
+ type RawTransactionRecord,
15
+ type TopicAnchorTip,
16
+ type TopicBlockAnchor,
17
+ extractMerkleProofMetadata
18
+ } from '../../BASM.js'
4
19
 
5
20
  const OUTPUT_SELECT_FIELDS = [
6
21
  'outputs.txid',
@@ -11,6 +26,7 @@ const OUTPUT_SELECT_FIELDS = [
11
26
  'outputs.outputsConsumed',
12
27
  'outputs.spent',
13
28
  'outputs.consumedBy',
29
+ 'outputs.blockHeight',
14
30
  'outputs.score'
15
31
  ] as const
16
32
 
@@ -38,13 +54,46 @@ export class KnexStorage implements Storage {
38
54
  return {
39
55
  ...row,
40
56
  outputScript: Array.from(row.outputScript),
41
- beef: includeBEEF ? (beefOverride ?? (row.beef !== undefined ? Array.from(row.beef) : undefined)) : undefined,
57
+ beef: (() => {
58
+ if (!includeBEEF) return undefined
59
+ if (beefOverride != null) return beefOverride
60
+ return row.beef != null ? Array.from(row.beef) : undefined
61
+ })(),
42
62
  spent: Boolean(row.spent),
43
63
  outputsConsumed: this.parseOutputRelations(row.outputsConsumed),
44
64
  consumedBy: this.parseOutputRelations(row.consumedBy)
45
65
  }
46
66
  }
47
67
 
68
+ private binaryToNumberArray(value: any): number[] | undefined {
69
+ if (value === undefined || value === null) {
70
+ return undefined
71
+ }
72
+ return Array.from(Buffer.from(value))
73
+ }
74
+
75
+ private binaryToHex(value: any): string | undefined {
76
+ const array = this.binaryToNumberArray(value)
77
+ if (array === undefined) {
78
+ return undefined
79
+ }
80
+ return Buffer.from(array).toString('hex')
81
+ }
82
+
83
+ private transactionRecordFromBEEF(txid: string, beef: number[]): StoredTransactionRecord {
84
+ const tx = Transaction.fromBEEF(beef)
85
+ const metadata = extractMerkleProofMetadata(txid, tx.merklePath)
86
+ return {
87
+ txid,
88
+ beef: tx.merklePath === undefined ? beef : tx.toAtomicBEEF(),
89
+ rawTx: Array.from(tx.toBinary()),
90
+ merklePath: tx.merklePath?.toBinary(),
91
+ blockHeight: metadata?.blockHeight,
92
+ blockIndex: metadata?.blockIndex,
93
+ merkleRoot: metadata?.merkleRoot
94
+ }
95
+ }
96
+
48
97
  private async fetchTransactionBeefMap(txids: string[]): Promise<Map<string, number[]>> {
49
98
  if (txids.length === 0) {
50
99
  return new Map<string, number[]>()
@@ -56,7 +105,7 @@ export class KnexStorage implements Storage {
56
105
 
57
106
  const beefByTxid = new Map<string, number[]>()
58
107
  for (const row of rows) {
59
- if (row.beef !== undefined) {
108
+ if (row.beef !== undefined && row.beef !== null) {
60
109
  beefByTxid.set(row.txid, Array.from(row.beef))
61
110
  }
62
111
  }
@@ -179,10 +228,10 @@ export class KnexStorage implements Storage {
179
228
  return outputs.map(output => this.parseOutputRecord(output, true, beefByTxid.get(output.txid)))
180
229
  }
181
230
 
182
- async deleteOutput (txid: string, outputIndex: number, _: string): Promise<void> {
231
+ async deleteOutput (txid: string, outputIndex: number, topic: string): Promise<void> {
183
232
  await this.knex.transaction(async trx => {
184
233
  // Delete the specific output
185
- await trx('outputs').where({ txid, outputIndex }).del()
234
+ await trx('outputs').where({ txid, outputIndex, topic }).del()
186
235
 
187
236
  // Check how many outputs reference the same transaction
188
237
  const remainingOutputs = await trx('outputs').where({ txid }).count('* as count').first()
@@ -212,15 +261,27 @@ export class KnexStorage implements Storage {
212
261
  outputsConsumed: JSON.stringify(output.outputsConsumed),
213
262
  consumedBy: JSON.stringify(output.consumedBy),
214
263
  spent: output.spent,
215
- score: output.score
264
+ score: output.score,
265
+ blockHeight: output.blockHeight
216
266
  })
217
267
  }
218
268
 
219
269
  if (output.beef !== undefined) {
220
- await trx('transactions').insert({
270
+ const record = this.transactionRecordFromBEEF(output.txid, output.beef)
271
+ const transactionRecord: Record<string, any> = {
221
272
  txid: output.txid,
222
- beef: Buffer.from(output.beef)
223
- }).onConflict('txid').ignore()
273
+ updatedAt: new Date()
274
+ }
275
+ if (record.beef !== undefined) transactionRecord.beef = Buffer.from(record.beef)
276
+ if (record.rawTx !== undefined) transactionRecord.rawTx = Buffer.from(record.rawTx)
277
+ if (record.merklePath !== undefined) transactionRecord.merklePath = Buffer.from(record.merklePath)
278
+ if (record.blockHeight !== undefined) transactionRecord.blockHeight = record.blockHeight
279
+ if (record.blockIndex !== undefined) transactionRecord.blockIndex = record.blockIndex
280
+ if (record.merkleRoot !== undefined) transactionRecord.merkleRoot = record.merkleRoot
281
+
282
+ const mergeRecord = { ...transactionRecord }
283
+ delete mergeRecord.txid
284
+ await trx('transactions').insert(transactionRecord).onConflict('txid').merge(mergeRecord)
224
285
  }
225
286
  })
226
287
  }
@@ -242,9 +303,7 @@ export class KnexStorage implements Storage {
242
303
  }
243
304
 
244
305
  async updateTransactionBEEF (txid: string, beef: number[]): Promise<void> {
245
- await this.knex('transactions').where({
246
- txid
247
- }).update('beef', Buffer.from(beef))
306
+ await this.upsertTransactionRecord(this.transactionRecordFromBEEF(txid, beef))
248
307
  }
249
308
 
250
309
  async updateOutputBlockHeight (txid: string, outputIndex: number, topic: string, blockHeight: number): Promise<void> {
@@ -255,10 +314,56 @@ export class KnexStorage implements Storage {
255
314
  }).update('blockHeight', blockHeight)
256
315
  }
257
316
 
258
- async insertAppliedTransaction (tx: { txid: string, topic: string }): Promise<void> {
317
+ async upsertTransactionRecord(record: StoredTransactionRecord): Promise<void> {
318
+ const insert: Record<string, any> = {
319
+ txid: record.txid,
320
+ updatedAt: new Date()
321
+ }
322
+ if (record.beef !== undefined) insert.beef = Buffer.from(record.beef)
323
+ if (record.rawTx !== undefined) insert.rawTx = Buffer.from(record.rawTx)
324
+ if (record.merklePath !== undefined) insert.merklePath = Buffer.from(record.merklePath)
325
+ if (record.blockHeight !== undefined) insert.blockHeight = record.blockHeight
326
+ if (record.blockHash !== undefined) insert.blockHash = record.blockHash
327
+ if (record.blockIndex !== undefined) insert.blockIndex = record.blockIndex
328
+ if (record.merkleRoot !== undefined) insert.merkleRoot = record.merkleRoot
329
+
330
+ const merge = { ...insert }
331
+ delete merge.txid
332
+
333
+ await this.knex('transactions')
334
+ .insert(insert)
335
+ .onConflict('txid')
336
+ .merge(merge)
337
+ }
338
+
339
+ async updateAppliedTransactionProof(record: AppliedTransactionProofUpdate): Promise<void> {
340
+ await this.knex('applied_transactions')
341
+ .where({ txid: record.txid, topic: record.topic })
342
+ .update({
343
+ blockHeight: record.blockHeight,
344
+ blockHash: record.blockHash,
345
+ blockIndex: record.blockIndex,
346
+ merkleRoot: record.merkleRoot,
347
+ proven: true
348
+ })
349
+
350
+ if (record.blockHeight !== undefined) {
351
+ await this.knex('outputs')
352
+ .where({ txid: record.txid, topic: record.topic })
353
+ .update({ blockHeight: record.blockHeight })
354
+ }
355
+ }
356
+
357
+ async insertAppliedTransaction (tx: AppliedTransaction): Promise<void> {
259
358
  await this.knex('applied_transactions').insert({
260
359
  txid: tx.txid,
261
- topic: tx.topic
360
+ topic: tx.topic,
361
+ blockHeight: tx.blockHeight,
362
+ blockHash: tx.blockHash,
363
+ blockIndex: tx.blockIndex,
364
+ merkleRoot: tx.merkleRoot,
365
+ firstSeenHeight: tx.firstSeenHeight,
366
+ proven: tx.proven ?? false
262
367
  })
263
368
  }
264
369
 
@@ -286,4 +391,261 @@ export class KnexStorage implements Storage {
286
391
 
287
392
  return result ? result.since : 0
288
393
  }
394
+
395
+ async findAdmittedTransactionsForBlock(topic: string, blockHeight: number, blockHash?: string): Promise<AdmittedTxRef[]> {
396
+ let query = this.knex('applied_transactions')
397
+ .where({ topic, blockHeight, proven: true })
398
+ .whereNotNull('blockIndex')
399
+
400
+ if (blockHash !== undefined) {
401
+ query = query.andWhere({ blockHash })
402
+ }
403
+
404
+ const rows = await query
405
+ .select(['txid', 'blockIndex'])
406
+ .orderBy('blockIndex', 'asc')
407
+
408
+ return rows.map(row => ({
409
+ txid: row.txid,
410
+ blockIndex: Number(row.blockIndex)
411
+ }))
412
+ }
413
+
414
+ async upsertTopicBlockAnchor(anchor: TopicBlockAnchor): Promise<void> {
415
+ await this.knex('topic_block_anchors')
416
+ .insert({
417
+ topic: anchor.topic,
418
+ blockHeight: anchor.blockHeight,
419
+ blockHash: anchor.blockHash,
420
+ basmRoot: anchor.basmRoot,
421
+ admittedCount: anchor.admittedCount,
422
+ tac: anchor.tac,
423
+ updatedAt: new Date()
424
+ })
425
+ .onConflict(['topic', 'blockHeight'])
426
+ .merge({
427
+ blockHash: anchor.blockHash,
428
+ basmRoot: anchor.basmRoot,
429
+ admittedCount: anchor.admittedCount,
430
+ tac: anchor.tac,
431
+ updatedAt: new Date()
432
+ })
433
+ }
434
+
435
+ async findTopicBlockAnchor(topic: string, blockHeight: number, blockHash?: string): Promise<TopicBlockAnchor | undefined> {
436
+ let query = this.knex('topic_block_anchors').where({ topic, blockHeight })
437
+ if (blockHash !== undefined) {
438
+ query = query.andWhere({ blockHash })
439
+ }
440
+ const row = await query.first()
441
+ if (row === undefined) {
442
+ return undefined
443
+ }
444
+ return {
445
+ topic: row.topic,
446
+ blockHeight: Number(row.blockHeight),
447
+ blockHash: row.blockHash,
448
+ basmRoot: row.basmRoot,
449
+ admittedCount: Number(row.admittedCount),
450
+ tac: row.tac
451
+ }
452
+ }
453
+
454
+ async findTopicBlockAnchors(topic: string, fromHeight: number, toHeight: number): Promise<TopicBlockAnchor[]> {
455
+ const rows = await this.knex('topic_block_anchors')
456
+ .where({ topic })
457
+ .andWhere('blockHeight', '>=', fromHeight)
458
+ .andWhere('blockHeight', '<=', toHeight)
459
+ .select(['topic', 'blockHeight', 'blockHash', 'basmRoot', 'admittedCount', 'tac'])
460
+ .orderBy('blockHeight', 'asc')
461
+
462
+ return rows.map(row => ({
463
+ topic: row.topic,
464
+ blockHeight: Number(row.blockHeight),
465
+ blockHash: row.blockHash,
466
+ basmRoot: row.basmRoot,
467
+ admittedCount: Number(row.admittedCount),
468
+ tac: row.tac
469
+ }))
470
+ }
471
+
472
+ async findTopicAnchorTip(topic: string): Promise<TopicAnchorTip | undefined> {
473
+ const row = await this.knex('topic_block_anchors')
474
+ .where({ topic })
475
+ .orderBy('blockHeight', 'desc')
476
+ .first()
477
+
478
+ if (row === undefined) {
479
+ return {
480
+ topic,
481
+ blockHeight: -1,
482
+ tac: BASM_ZERO_HASH
483
+ }
484
+ }
485
+
486
+ return {
487
+ topic: row.topic,
488
+ blockHeight: Number(row.blockHeight),
489
+ blockHash: row.blockHash,
490
+ basmRoot: row.basmRoot,
491
+ admittedCount: Number(row.admittedCount),
492
+ tac: row.tac
493
+ }
494
+ }
495
+
496
+ async findRawTransactions(txids: string[]): Promise<RawTransactionRecord[]> {
497
+ if (txids.length === 0) return []
498
+ const rows = await this.knex('transactions')
499
+ .whereIn('txid', txids)
500
+ .select(['txid', 'rawTx', 'beef'])
501
+
502
+ const records: RawTransactionRecord[] = []
503
+ for (const row of rows) {
504
+ let rawTx = this.binaryToHex(row.rawTx)
505
+ if (rawTx === undefined) {
506
+ const beef = this.binaryToNumberArray(row.beef)
507
+ if (beef !== undefined) {
508
+ rawTx = Transaction.fromBEEF(beef, row.txid).toHex()
509
+ }
510
+ }
511
+ if (rawTx !== undefined) {
512
+ records.push({ txid: row.txid, rawTx })
513
+ }
514
+ }
515
+ return records
516
+ }
517
+
518
+ async findTransactionMerklePaths(txids: string[]): Promise<Array<{
519
+ txid: string
520
+ merklePath: string
521
+ blockHeight?: number
522
+ blockHash?: string
523
+ blockIndex?: number
524
+ merkleRoot?: string
525
+ }>> {
526
+ if (txids.length === 0) return []
527
+ const rows = await this.knex('transactions')
528
+ .whereIn('txid', txids)
529
+ .select(['txid', 'merklePath', 'blockHeight', 'blockHash', 'blockIndex', 'merkleRoot', 'beef'])
530
+
531
+ const records: Array<{
532
+ txid: string
533
+ merklePath: string
534
+ blockHeight?: number
535
+ blockHash?: string
536
+ blockIndex?: number
537
+ merkleRoot?: string
538
+ }> = []
539
+
540
+ for (const row of rows) {
541
+ let merklePath = this.binaryToHex(row.merklePath)
542
+ if (merklePath === undefined) {
543
+ const beef = this.binaryToNumberArray(row.beef)
544
+ if (beef !== undefined) {
545
+ merklePath = Transaction.fromBEEF(beef, row.txid).merklePath?.toHex()
546
+ }
547
+ }
548
+ if (merklePath !== undefined) {
549
+ records.push({
550
+ txid: row.txid,
551
+ merklePath,
552
+ blockHeight: row.blockHeight === undefined || row.blockHeight === null ? undefined : Number(row.blockHeight),
553
+ blockHash: row.blockHash,
554
+ blockIndex: row.blockIndex === undefined || row.blockIndex === null ? undefined : Number(row.blockIndex),
555
+ merkleRoot: row.merkleRoot
556
+ })
557
+ }
558
+ }
559
+
560
+ return records
561
+ }
562
+
563
+ async findUnprovenAppliedTransactions(cutoffHeight: number, topic?: string): Promise<UnprovenAppliedTransactionCandidate[]> {
564
+ let query = this.knex('applied_transactions')
565
+ .where(builder => {
566
+ builder.where({ proven: false }).orWhereNull('proven')
567
+ })
568
+ .whereNotNull('firstSeenHeight')
569
+ .andWhere('firstSeenHeight', '<=', cutoffHeight)
570
+ .select(['txid', 'topic', 'firstSeenHeight'])
571
+
572
+ if (topic !== undefined) {
573
+ query = query.andWhere({ topic })
574
+ }
575
+
576
+ const rows = await query
577
+ const candidates: UnprovenAppliedTransactionCandidate[] = []
578
+
579
+ for (const row of rows) {
580
+ const outputs = await this.knex('outputs')
581
+ .where({ txid: row.txid, topic: row.topic })
582
+ .select(['txid', 'outputIndex'])
583
+
584
+ candidates.push({
585
+ txid: row.txid,
586
+ topic: row.topic,
587
+ firstSeenHeight: row.firstSeenHeight === undefined || row.firstSeenHeight === null ? undefined : Number(row.firstSeenHeight),
588
+ outputs: outputs.map(output => ({
589
+ txid: output.txid,
590
+ outputIndex: Number(output.outputIndex)
591
+ }))
592
+ })
593
+ }
594
+
595
+ return candidates
596
+ }
597
+
598
+ async deleteAppliedTransaction(txid: string, topic: string): Promise<void> {
599
+ await this.knex('applied_transactions').where({ txid, topic }).del()
600
+ }
601
+
602
+ async findProvenAppliedTransactionsByBlockHash(blockHash: string): Promise<Array<{ txid: string, topic: string, blockHeight: number }>> {
603
+ const rows = await this.knex('applied_transactions')
604
+ .where({ blockHash, proven: true })
605
+ .whereNotNull('blockHeight')
606
+ .select(['txid', 'topic', 'blockHeight'])
607
+
608
+ return rows.map(row => ({
609
+ txid: row.txid,
610
+ topic: row.topic,
611
+ blockHeight: Number(row.blockHeight)
612
+ }))
613
+ }
614
+
615
+ async findProvenAppliedTransactionsInRange(fromHeight: number, toHeight: number, topic?: string): Promise<Array<{ txid: string, topic: string, blockHeight: number, blockHash?: string, merkleRoot?: string }>> {
616
+ let query = this.knex('applied_transactions')
617
+ .where({ proven: true })
618
+ .andWhere('blockHeight', '>=', fromHeight)
619
+ .andWhere('blockHeight', '<=', toHeight)
620
+
621
+ if (topic !== undefined) {
622
+ query = query.andWhere({ topic })
623
+ }
624
+
625
+ const rows = await query.select(['txid', 'topic', 'blockHeight', 'blockHash', 'merkleRoot'])
626
+
627
+ return rows.map(row => ({
628
+ txid: row.txid,
629
+ topic: row.topic,
630
+ blockHeight: Number(row.blockHeight),
631
+ blockHash: row.blockHash ?? undefined,
632
+ merkleRoot: row.merkleRoot ?? undefined
633
+ }))
634
+ }
635
+
636
+ async demoteAppliedTransactionToUnproven(txid: string, topic: string): Promise<void> {
637
+ await this.knex('applied_transactions')
638
+ .where({ txid, topic })
639
+ .update({
640
+ blockHeight: null,
641
+ blockHash: null,
642
+ blockIndex: null,
643
+ merkleRoot: null,
644
+ proven: false
645
+ })
646
+
647
+ await this.knex('outputs')
648
+ .where({ txid, topic })
649
+ .update({ blockHeight: null })
650
+ }
289
651
  }
@@ -7,6 +7,7 @@ import { up as enlargeUp, down as enlargeDown } from './migrations/2025-05-28-00
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
9
  import { up as utxoLookupIndexUp, down as utxoLookupIndexDown } from './migrations/2025-11-11-001-utxo-lookup-index.js'
10
+ import { up as brc136BasmUp, down as brc136BasmDown } from './migrations/2026-05-29-001-brc136-basm.js'
10
11
 
11
12
  /**
12
13
  * An array of all migrations, in order.
@@ -24,7 +25,8 @@ const allMigrations: Migration[] = [
24
25
  { up: enlargeUp, down: enlargeDown },
25
26
  { up: gaspPaginationSupportUp, down: gaspPaginationSupportDown },
26
27
  { up: fixScoreColumnTypeUp, down: fixScoreColumnTypeDown },
27
- { up: utxoLookupIndexUp, down: utxoLookupIndexDown }
28
+ { up: utxoLookupIndexUp, down: utxoLookupIndexDown },
29
+ { up: brc136BasmUp, down: brc136BasmDown }
28
30
  ]
29
31
 
30
32
  export default allMigrations
@@ -4,24 +4,16 @@ import type { Knex } from 'knex'
4
4
  export async function up (knex: Knex): Promise<void> {
5
5
  const client = (knex.client.config.client || '').toLowerCase()
6
6
 
7
- /* ---------- outputs.outputScript ---------- */
8
7
  if (client.startsWith('mysql')) {
9
- // MySQL / MariaDB – enlarge to LONGBLOB (4 GB)
10
8
  await knex.schema.alterTable('outputs', table => {
11
9
  table.specificType('outputScript', 'LONGBLOB').alter()
12
10
  })
13
11
  } else if (client === 'sqlite3') {
14
- // SQLite – plain BLOB is already plenty, but run alter so Knex’s
15
- // internal schema snapshot stays in sync.
16
12
  await knex.schema.alterTable('outputs', table => {
17
13
  table.binary('outputScript').alter()
18
14
  })
19
15
  }
20
16
 
21
- /* ---------- transactions.beef ---------- */
22
- // This column was already created as LONGBLOB in migration 3,
23
- // but we enlarge defensively in case someone ran only the early
24
- // migrations on a new database.
25
17
  if (client.startsWith('mysql')) {
26
18
  await knex.schema.alterTable('transactions', table => {
27
19
  table.specificType('beef', 'LONGBLOB').alter()
@@ -36,25 +28,13 @@ export async function up (knex: Knex): Promise<void> {
36
28
  export async function down (knex: Knex): Promise<void> {
37
29
  const client = (knex.client.config.client || '').toLowerCase()
38
30
 
39
- /* ---------- outputs.outputScript ---------- */
40
- if (client.startsWith('mysql')) {
41
- // Revert to VARBINARY(255) – matches Knex’s default .binary()
42
- await knex.schema.alterTable('outputs', table => {
43
- table.binary('outputScript').alter()
44
- })
45
- } else if (client === 'sqlite3') {
46
- // No-op (SQLite stores the whole blob either way)
31
+ if (client.startsWith('mysql') || client === 'sqlite3') {
47
32
  await knex.schema.alterTable('outputs', table => {
48
33
  table.binary('outputScript').alter()
49
34
  })
50
35
  }
51
36
 
52
- /* ---------- transactions.beef ---------- */
53
- if (client.startsWith('mysql')) {
54
- await knex.schema.alterTable('transactions', table => {
55
- table.binary('beef').alter()
56
- })
57
- } else if (client === 'sqlite3') {
37
+ if (client.startsWith('mysql') || client === 'sqlite3') {
58
38
  await knex.schema.alterTable('transactions', table => {
59
39
  table.binary('beef').alter()
60
40
  })
@@ -0,0 +1,123 @@
1
+ import type { Knex } from 'knex'
2
+
3
+ function binaryLargeType(knex: Knex): string {
4
+ const client = String(knex.client.config.client ?? '').toLowerCase()
5
+ if (client.includes('mysql')) return 'longblob'
6
+ if (client.includes('pg')) return 'bytea'
7
+ return 'blob'
8
+ }
9
+
10
+ async function addColumnIfMissing(
11
+ knex: Knex,
12
+ tableName: string,
13
+ columnName: string,
14
+ addColumn: (table: Knex.TableBuilder) => void
15
+ ): Promise<void> {
16
+ const exists = await knex.schema.hasColumn(tableName, columnName)
17
+ if (!exists) {
18
+ await knex.schema.table(tableName, addColumn)
19
+ }
20
+ }
21
+
22
+ export async function up(knex: Knex): Promise<void> {
23
+ await addColumnIfMissing(knex, 'applied_transactions', 'blockHeight', table => {
24
+ table.integer('blockHeight').unsigned().nullable()
25
+ })
26
+ await addColumnIfMissing(knex, 'applied_transactions', 'blockHash', table => {
27
+ table.string('blockHash', 64).nullable()
28
+ })
29
+ await addColumnIfMissing(knex, 'applied_transactions', 'blockIndex', table => {
30
+ table.integer('blockIndex').unsigned().nullable()
31
+ })
32
+ await addColumnIfMissing(knex, 'applied_transactions', 'merkleRoot', table => {
33
+ table.string('merkleRoot', 64).nullable()
34
+ })
35
+ await addColumnIfMissing(knex, 'applied_transactions', 'firstSeenHeight', table => {
36
+ table.integer('firstSeenHeight').unsigned().nullable()
37
+ })
38
+ await addColumnIfMissing(knex, 'applied_transactions', 'proven', table => {
39
+ table.boolean('proven').notNullable().defaultTo(false)
40
+ })
41
+
42
+ await addColumnIfMissing(knex, 'transactions', 'rawTx', table => {
43
+ table.specificType('rawTx', binaryLargeType(knex)).nullable()
44
+ })
45
+ await addColumnIfMissing(knex, 'transactions', 'merklePath', table => {
46
+ table.specificType('merklePath', binaryLargeType(knex)).nullable()
47
+ })
48
+ await addColumnIfMissing(knex, 'transactions', 'blockHeight', table => {
49
+ table.integer('blockHeight').unsigned().nullable()
50
+ })
51
+ await addColumnIfMissing(knex, 'transactions', 'blockHash', table => {
52
+ table.string('blockHash', 64).nullable()
53
+ })
54
+ await addColumnIfMissing(knex, 'transactions', 'blockIndex', table => {
55
+ table.integer('blockIndex').unsigned().nullable()
56
+ })
57
+ await addColumnIfMissing(knex, 'transactions', 'merkleRoot', table => {
58
+ table.string('merkleRoot', 64).nullable()
59
+ })
60
+ await addColumnIfMissing(knex, 'transactions', 'updatedAt', table => {
61
+ table.timestamp('updatedAt').nullable()
62
+ })
63
+
64
+ const topicBlockAnchorsExists = await knex.schema.hasTable('topic_block_anchors')
65
+ if (!topicBlockAnchorsExists) {
66
+ await knex.schema.createTable('topic_block_anchors', table => {
67
+ table.increments()
68
+ table.string('topic').notNullable()
69
+ table.integer('blockHeight').unsigned().notNullable()
70
+ table.string('blockHash', 64).notNullable()
71
+ table.string('basmRoot', 64).notNullable()
72
+ table.integer('admittedCount').unsigned().notNullable()
73
+ table.string('tac', 64).notNullable()
74
+ table.timestamp('updatedAt').nullable()
75
+ table.unique(['topic', 'blockHeight'], 'idx_topic_block_anchors_topic_height_unique')
76
+ table.index(['topic', 'blockHeight'], 'idx_topic_block_anchors_topic_height')
77
+ table.index(['topic', 'blockHash'], 'idx_topic_block_anchors_topic_hash')
78
+ })
79
+ }
80
+
81
+ const syncStateExists = await knex.schema.hasTable('basm_sync_state')
82
+ if (!syncStateExists) {
83
+ await knex.schema.createTable('basm_sync_state', table => {
84
+ table.string('host').notNullable()
85
+ table.string('topic').notNullable()
86
+ table.integer('lastHeight').unsigned().notNullable().defaultTo(0)
87
+ table.timestamp('updatedAt').nullable()
88
+ table.primary(['host', 'topic'])
89
+ table.index('topic')
90
+ })
91
+ }
92
+
93
+ const appliedTopicHeightIndex = 'idx_applied_transactions_topic_height'
94
+ const hasAppliedTopicHeightIndex = await knex.schema.hasTable('applied_transactions')
95
+ if (hasAppliedTopicHeightIndex) {
96
+ await knex.schema.table('applied_transactions', table => {
97
+ table.index(['topic', 'blockHeight', 'blockHash', 'blockIndex'], appliedTopicHeightIndex)
98
+ table.index(['topic', 'proven', 'firstSeenHeight'], 'idx_applied_transactions_unproven')
99
+ })
100
+ }
101
+ }
102
+
103
+ export async function down(knex: Knex): Promise<void> {
104
+ await knex.schema.dropTableIfExists('basm_sync_state')
105
+ await knex.schema.dropTableIfExists('topic_block_anchors')
106
+
107
+ await knex.schema.table('applied_transactions', table => {
108
+ table.dropIndex(['topic', 'blockHeight', 'blockHash', 'blockIndex'], 'idx_applied_transactions_topic_height')
109
+ table.dropIndex(['topic', 'proven', 'firstSeenHeight'], 'idx_applied_transactions_unproven')
110
+ })
111
+
112
+ for (const column of ['updatedAt', 'merkleRoot', 'blockIndex', 'blockHash', 'blockHeight', 'merklePath', 'rawTx']) {
113
+ if (await knex.schema.hasColumn('transactions', column)) {
114
+ await knex.schema.table('transactions', table => table.dropColumn(column))
115
+ }
116
+ }
117
+
118
+ for (const column of ['proven', 'firstSeenHeight', 'merkleRoot', 'blockIndex', 'blockHash', 'blockHeight']) {
119
+ if (await knex.schema.hasColumn('applied_transactions', column)) {
120
+ await knex.schema.table('applied_transactions', table => table.dropColumn(column))
121
+ }
122
+ }
123
+ }