@bsv/overlay 2.0.3 → 2.1.0
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/README.md +226 -106
- package/dist/cjs/mod.js +6 -1
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +21 -11
- package/dist/cjs/src/BASM.js +92 -0
- package/dist/cjs/src/BASM.js.map +1 -0
- package/dist/cjs/src/BASMRemote.js +43 -0
- package/dist/cjs/src/BASMRemote.js.map +1 -0
- package/dist/cjs/src/Engine.js +649 -47
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPRemote.js +2 -2
- package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/cjs/src/GASP/OverlayGASPStorage.js +18 -25
- package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/cjs/src/storage/knex/KnexStorage.js +329 -12
- package/dist/cjs/src/storage/knex/KnexStorage.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-05-28-001-enlarge.js +2 -24
- package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
- package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js +114 -0
- package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -0
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +1 -0
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/BASM.js +85 -0
- package/dist/esm/src/BASM.js.map +1 -0
- package/dist/esm/src/BASMRemote.js +42 -0
- package/dist/esm/src/BASMRemote.js.map +1 -0
- package/dist/esm/src/Engine.js +637 -46
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPRemote.js +2 -2
- package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
- package/dist/esm/src/GASP/OverlayGASPStorage.js +18 -25
- package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
- package/dist/esm/src/storage/knex/KnexStorage.js +323 -12
- package/dist/esm/src/storage/knex/KnexStorage.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-05-28-001-enlarge.js +2 -24
- package/dist/esm/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
- package/dist/esm/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js +109 -0
- package/dist/esm/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -0
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +2 -0
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/BASM.d.ts +110 -0
- package/dist/types/src/BASM.d.ts.map +1 -0
- package/dist/types/src/BASMRemote.d.ts +14 -0
- package/dist/types/src/BASMRemote.d.ts.map +1 -0
- package/dist/types/src/Engine.d.ts +78 -1
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts +1 -1
- package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
- package/dist/types/src/LookupService.d.ts.map +1 -1
- package/dist/types/src/storage/Storage.d.ts +118 -0
- package/dist/types/src/storage/Storage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/KnexStorage.d.ts +38 -6
- package/dist/types/src/storage/knex/KnexStorage.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-05-28-001-enlarge.d.ts.map +1 -1
- package/dist/types/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts +4 -0
- package/dist/types/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts.map +1 -0
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/docs/API.md +72 -580
- package/docs/BRC-136-BASM.md +285 -0
- package/docs/README.md +2 -0
- package/docs/Synchronization.md +10 -1
- package/docs/examples/README.md +2 -2
- package/docs/examples/gs-wip.md +68 -76
- package/mod.ts +25 -1
- package/package.json +33 -23
- package/src/BASM.ts +208 -0
- package/src/BASMRemote.ts +54 -0
- package/src/Engine.ts +762 -47
- package/src/GASP/OverlayGASPRemote.ts +2 -2
- package/src/GASP/OverlayGASPStorage.ts +17 -22
- package/src/LookupService.ts +2 -1
- package/src/__tests/BASM.test.ts +44 -0
- package/src/__tests/BASMChain.test.ts +290 -0
- package/src/__tests/Engine.test.ts +3 -4
- package/src/storage/Storage.ts +127 -0
- package/src/storage/knex/KnexStorage.ts +376 -14
- package/src/storage/knex/all-migrations.ts +3 -1
- package/src/storage/knex/migrations/2025-05-28-001-enlarge.ts +2 -22
- package/src/storage/knex/migrations/2026-05-29-001-brc136-basm.ts +123 -0
|
@@ -1,6 +1,21 @@
|
|
|
1
|
-
import {
|
|
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:
|
|
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,
|
|
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
|
-
|
|
270
|
+
const record = this.transactionRecordFromBEEF(output.txid, output.beef)
|
|
271
|
+
const transactionRecord: Record<string, any> = {
|
|
221
272
|
txid: output.txid,
|
|
222
|
-
|
|
223
|
-
}
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
+
}
|