@bsv/overlay 2.0.1 → 2.0.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.
@@ -4,11 +4,11 @@
4
4
 
5
5
  ---
6
6
 
7
- Here, you will find documentation for common example usages of the Overlay Services Engine.
7
+ Here, you will find documentation for common example usages of the Overlay Services Engine. These examples focus on direct `@bsv/overlay` integration. For a ready-to-run HTTP overlay node, use [`@bsv/overlay-express`](https://github.com/bsv-blockchain/overlay-express) and [`overlay-express-examples`](https://github.com/bsv-blockchain/overlay-express-examples).
8
8
 
9
9
  ## Available Examples
10
10
 
11
- - [Getting Started (WIP)](./gs-wip.md) — Introduction and getting started guide
11
+ - [Getting Started](./gs-wip.md) — Introduction and low-level engine setup guide
12
12
 
13
13
  ---
14
14
 
@@ -6,106 +6,114 @@
6
6
 
7
7
  ## Introduction to BSV Overlay Services Engine
8
8
 
9
- The BSV Overlay Services Engine is designed to process transactions and manage data within a blockchain-based system, specifically targeting the Bitcoin SV (BSV) blockchain. It integrates various components such as Topic Managers, Lookup Services, Storage, and Chain Tracker to provide a robust environment for managing transaction data and overlay services.
9
+ The BSV Overlay Services Engine is the low-level runtime for topic managers and lookup services. It validates candidate outputs, stores admitted UTXOs, tracks spends, answers lookup questions, and coordinates synchronization between overlay nodes.
10
+
11
+ If you want to run an HTTP overlay node, start with [`@bsv/overlay-express`](https://github.com/bsv-blockchain/overlay-express). If you want a local or cloud application runtime, use LARS or CARS with the BRC-102 `deployment-info.json` structure. Use `@bsv/overlay` directly when you are building custom infrastructure around the engine.
10
12
 
11
13
  ### Components of the System
12
14
 
13
- 1. **Topic Managers**: Responsible for managing the admittance of transactions related to specific topics.
14
- 2. **Lookup Services**: Handle the lookup of UTXO (Unspent Transaction Output) data for transactions.
15
- 3. **Storage**: Manages persistent data storage, tracking UTXOs and their states within the system.
16
- 4. **Chain Tracker**: Verifies SPV (Simplified Payment Verification) data associated with transactions to ensure their validity.
15
+ 1. **Topic Managers** decide which transaction outputs are admitted for a topic.
16
+ 2. **Lookup Services** index admitted and spent outputs and answer domain-specific lookup questions.
17
+ 3. **Storage** persists admitted outputs, spend state, history, sync state, and interaction timestamps.
18
+ 4. **Chain Tracker** verifies SPV data for transactions unless the engine is configured for script-only validation.
19
+ 5. **Broadcaster** submits accepted transactions to the network.
20
+ 6. **Advertiser** publishes SHIP and SLAP availability records when peer discovery is enabled.
17
21
 
18
22
  ### Setting Up the Engine
19
23
 
20
- Before you can use the engine, you must initialize it with the required components:
21
-
22
- ```ts
23
- import { Engine, KnexStorage } from "@bsv/overlay";
24
- import { HelloTopicManager, HelloLookupService } from 'hello-services';
25
- import { WoChain } from "@bsv/sdk";
26
-
27
- // Initialize components
28
- const managers = {
29
- "exampleTopic": new HelloTopicManager()
30
- };
24
+ Install the current packages:
31
25
 
32
- const lookupServices = {
33
- "exampleLookup": new HelloLookupService()
34
- };
26
+ ```bash
27
+ npm i @bsv/overlay @bsv/sdk knex
28
+ ```
35
29
 
36
- const storage = new KnexStorage();
37
- const chainTracker = new WoChain();
30
+ Create the engine with your concrete implementations:
38
31
 
39
- // Create the engine instance
40
- const engine = new Engine(managers, lookupServices, storage, chainTracker);
32
+ ```ts
33
+ import { Engine, KnexStorage } from '@bsv/overlay'
34
+ import { WhatsOnChain, NodejsHttpClient, ARC } from '@bsv/sdk'
35
+ import knexFactory from 'knex'
36
+
37
+ import { ExampleLookupService } from './services/ExampleLookupService.js'
38
+ import { ExampleTopicManager } from './services/ExampleTopicManager.js'
39
+
40
+ const knex = knexFactory({
41
+ client: 'mysql2',
42
+ connection: process.env.KNEX_URL
43
+ })
44
+
45
+ const engine = new Engine(
46
+ {
47
+ tm_example: new ExampleTopicManager()
48
+ },
49
+ {
50
+ ls_example: new ExampleLookupService()
51
+ },
52
+ new KnexStorage(knex),
53
+ new WhatsOnChain('test', { httpClient: new NodejsHttpClient() }),
54
+ process.env.HOSTING_URL,
55
+ process.env.SHIP_TRACKERS?.split(',') ?? [],
56
+ process.env.SLAP_TRACKERS?.split(',') ?? [],
57
+ new ARC(process.env.ARC_URL ?? 'https://arc.taal.com', {
58
+ apiKey: process.env.ARC_API_KEY
59
+ }),
60
+ undefined,
61
+ {
62
+ tm_example: 'SHIP'
63
+ }
64
+ )
41
65
  ```
42
66
 
67
+ Use the string `'scripts only'` for the chain tracker only when the service intentionally skips SPV validation and relies only on script-level checks.
68
+
43
69
  ### Submitting a Transaction
44
70
 
45
- To submit a transaction for processing by the Overlay Services:
71
+ Submit tagged BEEF bytes with the topics that should evaluate the transaction:
46
72
 
47
73
  ```ts
48
74
  import { Transaction } from '@bsv/sdk'
49
75
 
50
- const tx = new Transaction(/* ... */);
76
+ const tx = new Transaction(/* ... */)
51
77
 
52
- const transaction = {
53
- beef: tx.toBEEF(),
54
- topics: ['exampleTopic']
55
- }
78
+ const steak = await engine.submit({
79
+ beef: tx.toBEEF(),
80
+ topics: ['tm_example']
81
+ })
56
82
 
57
- // Submit transaction
58
- engine.submit(transaction).then(steak => {
59
- console.log("Transaction processed:", steak);
60
- }).catch(error => {
61
- console.error("Error processing transaction:", error);
62
- });
83
+ console.log('Transaction processed:', steak)
63
84
  ```
64
85
 
65
86
  ### Lookup Queries
66
87
 
67
- To perform a lookup query using the engine:
88
+ Ask a lookup service a domain-specific question:
68
89
 
69
90
  ```ts
70
- const question = {
71
- service: 'exampleLookup',
72
- query: {
73
- name: 'Bob'
74
- }
75
- }
76
-
77
- // Perform a lookup
78
- engine.lookup(question).then(answer => {
79
- console.log("Lookup result:", answer);
80
- }).catch(error => {
81
- console.error("Error performing lookup:", error);
82
- });
91
+ const answer = await engine.lookup({
92
+ service: 'ls_example',
93
+ query: {
94
+ identityKey: '03...'
95
+ }
96
+ })
97
+
98
+ console.log('Lookup result:', answer)
83
99
  ```
84
100
 
85
- ### Managing UTXOs
101
+ ### Service Documentation
86
102
 
87
- The system's core functionality involves managing UTXOs:
103
+ Overlay clients and dashboards can retrieve service documentation and metadata directly from the engine:
88
104
 
89
- 1. **Inserting a New UTXO**: Store new UTXO data when transactions are processed.
90
- 2. **Deleting a UTXO**: Remove UTXOs that are no longer needed or have been consumed by newer transactions.
91
- 3. **Tracking UTXO Consumption**: Monitor which transactions consume which UTXOs.
105
+ ```ts
106
+ const topicDocs = await engine.getDocumentationForTopicManger('tm_example')
107
+ const lookupDocs = await engine.getDocumentationForLookupServiceProvider('ls_example')
92
108
 
93
- ### Retrieving Documentation
109
+ const topics = await engine.listTopicManagers()
110
+ const lookupServices = await engine.listLookupServiceProviders()
111
+ ```
94
112
 
95
- To retrieve documentation for specific managers or services:
113
+ ### Deployment Path
96
114
 
97
- ```ts
98
- // For a topic manager
99
- engine.getDocumentationForTopicManger("exampleTopic").then(doc => {
100
- console.log("Documentation for Topic Manager:", doc);
101
- });
102
-
103
- // For a lookup service
104
- engine.getDocumentationForLookupServiceProvider("exampleLookup").then(doc => {
105
- console.log("Documentation for Lookup Service:", doc);
106
- });
107
- ```
115
+ For a public overlay node, wire the engine into HTTP routes using `@bsv/overlay-express` instead of hand-rolling routes. Overlay Express already exposes the standard submit, lookup, sync, health, documentation, and admin surfaces expected by the rest of the BSV overlay ecosystem.
108
116
 
109
117
  ### Conclusion
110
118
 
111
- The BSV Overlay Services Engine provides a powerful toolset for managing transactions and data on the Bitcoin SV blockchain. It's designed to handle complex data structures and ensure the integrity and security of transactions through rigorous validation and management processes. By following this tutorial, developers can effectively integrate and utilize these capabilities within their blockchain applications.
119
+ The BSV Overlay Services Engine is the shared core for overlay validation, indexing, lookup, and synchronization. Keep direct engine usage focused on infrastructure-level integrations, and use Overlay Express, LARS, and CARS for the standard application path.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bsv/overlay",
3
- "version": "2.0.1",
3
+ "version": "2.0.3",
4
4
  "type": "module",
5
5
  "description": "BSV Blockchain Overlay Services Engine",
6
6
  "main": "dist/cjs/mod.js",
@@ -74,7 +74,7 @@
74
74
  },
75
75
  "dependencies": {
76
76
  "@bsv/gasp": "^1.2.2",
77
- "@bsv/sdk": "^2.0.4",
77
+ "@bsv/sdk": "^2.0.14",
78
78
  "knex": "^3.1.0"
79
79
  }
80
80
  }
package/src/Engine.ts CHANGED
@@ -552,6 +552,10 @@ export class Engine {
552
552
 
553
553
  const lookupResult = await lookupService.lookup(lookupQuestion)
554
554
  const hydrationContext = this.createUTXOHistoryHydrationContext()
555
+ await this.preloadOutputsWithBEEF(
556
+ lookupResult.map(({ txid, outputIndex }) => ({ txid, outputIndex })),
557
+ hydrationContext
558
+ )
555
559
  const hydratedOutputs = (await Promise.all(
556
560
  lookupResult.map(async ({ txid, outputIndex, history, context }) => {
557
561
  const UTXO = await this.loadOutputWithBEEF(txid, outputIndex, hydrationContext)
@@ -590,12 +594,66 @@ export class Engine {
590
594
  }
591
595
  }
592
596
 
597
+ private toOutputCacheKey(txid: string, outputIndex: number): string {
598
+ return `${txid}:${outputIndex}`
599
+ }
600
+
601
+ private async preloadOutputsWithBEEF(
602
+ outpoints: Array<{ txid: string, outputIndex: number }>,
603
+ context: UTXOHistoryHydrationContext
604
+ ): Promise<void> {
605
+ if (outpoints.length === 0) {
606
+ return
607
+ }
608
+
609
+ const deduped: Array<{ txid: string, outputIndex: number }> = []
610
+ const seen = new Set<string>()
611
+
612
+ for (const outpoint of outpoints) {
613
+ const cacheKey = this.toOutputCacheKey(outpoint.txid, outpoint.outputIndex)
614
+ if (seen.has(cacheKey)) {
615
+ continue
616
+ }
617
+ seen.add(cacheKey)
618
+ if (!context.outputCache.has(cacheKey)) {
619
+ deduped.push(outpoint)
620
+ }
621
+ }
622
+
623
+ if (deduped.length === 0) {
624
+ return
625
+ }
626
+
627
+ const findOutputsByOutpoints = this.storage.findOutputsByOutpoints
628
+ if (typeof findOutputsByOutpoints === 'function') {
629
+ const outputs = await findOutputsByOutpoints.call(this.storage, deduped, true)
630
+ const outputsByKey = new Map<string, Output>()
631
+ for (const output of outputs) {
632
+ outputsByKey.set(this.toOutputCacheKey(output.txid, output.outputIndex), output)
633
+ }
634
+
635
+ for (const outpoint of deduped) {
636
+ const cacheKey = this.toOutputCacheKey(outpoint.txid, outpoint.outputIndex)
637
+ context.outputCache.set(cacheKey, Promise.resolve(outputsByKey.get(cacheKey) ?? null))
638
+ }
639
+ return
640
+ }
641
+
642
+ for (const outpoint of deduped) {
643
+ const cacheKey = this.toOutputCacheKey(outpoint.txid, outpoint.outputIndex)
644
+ context.outputCache.set(
645
+ cacheKey,
646
+ this.storage.findOutput(outpoint.txid, outpoint.outputIndex, undefined, undefined, true)
647
+ )
648
+ }
649
+ }
650
+
593
651
  private async loadOutputWithBEEF(
594
652
  txid: string,
595
653
  outputIndex: number,
596
654
  context: UTXOHistoryHydrationContext
597
655
  ): Promise<Output | null> {
598
- const cacheKey = `${txid}:${outputIndex}`
656
+ const cacheKey = this.toOutputCacheKey(txid, outputIndex)
599
657
  let cached = context.outputCache.get(cacheKey)
600
658
  if (cached === undefined) {
601
659
  cached = this.storage.findOutput(txid, outputIndex, undefined, undefined, true)
@@ -626,6 +684,8 @@ export class Engine {
626
684
  return undefined
627
685
  }
628
686
 
687
+ await this.preloadOutputsWithBEEF(output.outputsConsumed, context)
688
+
629
689
  const childNodes = (await Promise.all(
630
690
  output.outputsConsumed.map(async (outputIdentifier) => {
631
691
  const childOutput = await this.loadOutputWithBEEF(outputIdentifier.txid, outputIdentifier.outputIndex, context)
@@ -638,15 +698,21 @@ export class Engine {
638
698
  )).filter((node): node is HydratedUTXOHistoryNode => node !== undefined)
639
699
 
640
700
  const tx = Transaction.fromBEEF(output.beef)
701
+ const inputIndexBySource = new Map<string, number>()
702
+ tx.inputs.forEach((candidateInput, index) => {
703
+ const sourceTXID = candidateInput.sourceTXID !== undefined && candidateInput.sourceTXID !== ''
704
+ ? candidateInput.sourceTXID
705
+ : candidateInput.sourceTransaction?.id('hex')
641
706
 
642
- for (const child of childNodes) {
643
- const inputIndex = tx.inputs.findIndex((candidateInput) => {
644
- const sourceTXID = candidateInput.sourceTXID !== undefined && candidateInput.sourceTXID !== ''
645
- ? candidateInput.sourceTXID
646
- : candidateInput.sourceTransaction?.id('hex')
707
+ if (sourceTXID === undefined) {
708
+ return
709
+ }
647
710
 
648
- return sourceTXID === child.output.txid && candidateInput.sourceOutputIndex === child.output.outputIndex
649
- })
711
+ inputIndexBySource.set(`${sourceTXID}:${candidateInput.sourceOutputIndex}`, index)
712
+ })
713
+
714
+ for (const child of childNodes) {
715
+ const inputIndex = inputIndexBySource.get(`${child.output.txid}:${child.output.outputIndex}`)
650
716
 
651
717
  if (inputIndex === -1 || inputIndex == null) {
652
718
  continue
@@ -845,6 +845,34 @@ describe('BSV Overlay Services Engine', () => {
845
845
  query: { name: 'Bob' }
846
846
  })
847
847
  })
848
+ it('Uses batched output loading when the storage engine supports it', async () => {
849
+ mockLookupService.lookup = jest.fn(async () => [{
850
+ txid: 'mockTXID',
851
+ outputIndex: 0,
852
+ history: undefined
853
+ }])
854
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
855
+ const findOutputsByOutpoints = jest.fn(async () => [mockOutput])
856
+ ;(mockStorageEngine).findOutputsByOutpoints = findOutputsByOutpoints
857
+ const engine = new Engine(
858
+ {
859
+ Hello: mockTopicManager
860
+ },
861
+ {
862
+ Hello: mockLookupService
863
+ },
864
+ mockStorageEngine,
865
+ mockChainTracker
866
+ )
867
+
868
+ await engine.lookup({
869
+ service: 'Hello',
870
+ query: { name: 'Bob' }
871
+ })
872
+
873
+ expect(findOutputsByOutpoints).toHaveBeenCalledWith([{ txid: 'mockTXID', outputIndex: 0 }], true)
874
+ expect(mockStorageEngine.findOutput).not.toHaveBeenCalled()
875
+ })
848
876
  describe('For each returned result', () => {
849
877
  it('Finds the identified UTXO by its txid and vout', async () => {
850
878
  // TODO: Make the default storage engine return something...?
@@ -30,6 +30,17 @@ export interface Storage {
30
30
  */
31
31
  findOutput: (txid: string, outputIndex: number, topic?: string, spent?: boolean, includeBEEF?: boolean) => Promise<Output | null>
32
32
 
33
+ /**
34
+ * Finds multiple outputs from storage by txid/output index pairs.
35
+ * Implementations can use this to collapse many point lookups into a single query.
36
+ * @param outpoints — txid/output index pairs to find
37
+ * @param includeBEEF — Whether to include the BEEF data for the outputs (optional)
38
+ */
39
+ findOutputsByOutpoints?: (
40
+ outpoints: Array<{ txid: string, outputIndex: number }>,
41
+ includeBEEF?: boolean
42
+ ) => Promise<Output[]>
43
+
33
44
  /**
34
45
  * Finds outputs with a matching transaction ID from storage
35
46
  * @param txid — TXID of the outputs to find
@@ -2,6 +2,18 @@ import { Storage } from '../Storage.js'
2
2
  import { Knex } from 'knex'
3
3
  import type { Output } from '../../Output.js'
4
4
 
5
+ const OUTPUT_SELECT_FIELDS = [
6
+ 'outputs.txid',
7
+ 'outputs.outputIndex',
8
+ 'outputs.outputScript',
9
+ 'outputs.topic',
10
+ 'outputs.satoshis',
11
+ 'outputs.outputsConsumed',
12
+ 'outputs.spent',
13
+ 'outputs.consumedBy',
14
+ 'outputs.score'
15
+ ] as const
16
+
5
17
  export class KnexStorage implements Storage {
6
18
  knex: Knex
7
19
 
@@ -9,6 +21,48 @@ export class KnexStorage implements Storage {
9
21
  this.knex = knex
10
22
  }
11
23
 
24
+ private parseOutputRelations(
25
+ value: string | Array<{ txid: string, outputIndex: number }>
26
+ ): Array<{ txid: string, outputIndex: number }> {
27
+ if (Array.isArray(value)) {
28
+ return value
29
+ }
30
+ return JSON.parse(value)
31
+ }
32
+
33
+ private parseOutputRecord(
34
+ row: any,
35
+ includeBEEF: boolean,
36
+ beefOverride?: number[]
37
+ ): Output {
38
+ return {
39
+ ...row,
40
+ outputScript: Array.from(row.outputScript),
41
+ beef: includeBEEF ? (beefOverride ?? (row.beef !== undefined ? Array.from(row.beef) : undefined)) : undefined,
42
+ spent: Boolean(row.spent),
43
+ outputsConsumed: this.parseOutputRelations(row.outputsConsumed),
44
+ consumedBy: this.parseOutputRelations(row.consumedBy)
45
+ }
46
+ }
47
+
48
+ private async fetchTransactionBeefMap(txids: string[]): Promise<Map<string, number[]>> {
49
+ if (txids.length === 0) {
50
+ return new Map<string, number[]>()
51
+ }
52
+
53
+ const rows = await this.knex('transactions')
54
+ .whereIn('txid', txids)
55
+ .select(['txid', 'beef'])
56
+
57
+ const beefByTxid = new Map<string, number[]>()
58
+ for (const row of rows) {
59
+ if (row.beef !== undefined) {
60
+ beefByTxid.set(row.txid, Array.from(row.beef))
61
+ }
62
+ }
63
+ return beefByTxid
64
+ }
65
+
12
66
  async findOutput (txid: string, outputIndex: number, topic?: string, spent?: boolean, includeBEEF: boolean = false): Promise<Output | null> {
13
67
  const search: {
14
68
  'outputs.txid': string
@@ -22,21 +76,8 @@ export class KnexStorage implements Storage {
22
76
  if (topic !== undefined) search['outputs.topic'] = topic
23
77
  if (spent !== undefined) search['outputs.spent'] = spent
24
78
 
25
- // Base query to get the output
26
79
  const query = this.knex('outputs').where(search)
27
-
28
- // Select necessary fields from outputs and conditionally include beef from transactions
29
- const selectFields = [
30
- 'outputs.txid',
31
- 'outputs.outputIndex',
32
- 'outputs.outputScript',
33
- 'outputs.topic',
34
- 'outputs.satoshis',
35
- 'outputs.outputsConsumed',
36
- 'outputs.spent',
37
- 'outputs.consumedBy',
38
- 'outputs.score'
39
- ]
80
+ const selectFields: string[] = [...OUTPUT_SELECT_FIELDS]
40
81
 
41
82
  if (includeBEEF) {
42
83
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
@@ -50,53 +91,57 @@ export class KnexStorage implements Storage {
50
91
  return null
51
92
  }
52
93
 
53
- return {
54
- ...output,
55
- outputScript: [...output.outputScript],
56
- beef: includeBEEF ? (output.beef !== undefined ? [...output.beef] : undefined) : undefined,
57
- spent: Boolean(output.spent),
58
- outputsConsumed: JSON.parse(output.outputsConsumed),
59
- consumedBy: JSON.parse(output.consumedBy)
60
- }
94
+ return this.parseOutputRecord(output, includeBEEF)
61
95
  }
62
96
 
63
- async findOutputsForTransaction (txid: string, includeBEEF: boolean = false): Promise<Output[]> {
64
- // Base query to get outputs
65
- const query = this.knex('outputs').where({ 'outputs.txid': txid })
66
-
67
- // Select necessary fields from outputs and conditionally include beef from transactions
68
- const selectFields = [
69
- 'outputs.txid',
70
- 'outputs.outputIndex',
71
- 'outputs.outputScript',
72
- 'outputs.topic',
73
- 'outputs.satoshis',
74
- 'outputs.outputsConsumed',
75
- 'outputs.spent',
76
- 'outputs.consumedBy',
77
- 'outputs.score'
78
- ]
97
+ async findOutputsByOutpoints (
98
+ outpoints: Array<{ txid: string, outputIndex: number }>,
99
+ includeBEEF: boolean = false
100
+ ): Promise<Output[]> {
101
+ if (outpoints.length === 0) {
102
+ return []
103
+ }
79
104
 
80
- if (includeBEEF) {
81
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
82
- query.leftJoin('transactions', 'outputs.txid', 'transactions.txid')
83
- selectFields.push('transactions.beef')
105
+ const deduped = new Map<string, { txid: string, outputIndex: number }>()
106
+ for (const outpoint of outpoints) {
107
+ deduped.set(`${outpoint.txid}:${outpoint.outputIndex}`, outpoint)
108
+ }
109
+
110
+ const rows = await this.knex('outputs')
111
+ .whereIn(
112
+ ['outputs.txid', 'outputs.outputIndex'],
113
+ Array.from(deduped.values()).map(outpoint => [outpoint.txid, outpoint.outputIndex])
114
+ )
115
+ .select([...OUTPUT_SELECT_FIELDS])
116
+
117
+ if (rows === undefined || rows.length === 0) {
118
+ return []
119
+ }
120
+
121
+ if (!includeBEEF) {
122
+ return rows.map(row => this.parseOutputRecord(row, false))
84
123
  }
85
124
 
86
- const outputs = await query.select(selectFields)
125
+ const txids = Array.from(new Set(rows.map(row => row.txid)))
126
+ const beefByTxid = await this.fetchTransactionBeefMap(txids)
127
+ return rows.map(row => this.parseOutputRecord(row, true, beefByTxid.get(row.txid)))
128
+ }
129
+
130
+ async findOutputsForTransaction (txid: string, includeBEEF: boolean = false): Promise<Output[]> {
131
+ const outputs = await this.knex('outputs')
132
+ .where({ 'outputs.txid': txid })
133
+ .select([...OUTPUT_SELECT_FIELDS])
87
134
 
88
135
  if (outputs === undefined || outputs.length === 0) {
89
136
  return []
90
137
  }
91
138
 
92
- return outputs.map(output => ({
93
- ...output,
94
- outputScript: [...output.outputScript],
95
- beef: includeBEEF ? (output.beef !== undefined ? [...output.beef] : undefined) : undefined,
96
- spent: Boolean(output.spent),
97
- outputsConsumed: JSON.parse(output.outputsConsumed),
98
- consumedBy: JSON.parse(output.consumedBy)
99
- }))
139
+ if (!includeBEEF) {
140
+ return outputs.map(output => this.parseOutputRecord(output, false))
141
+ }
142
+
143
+ const beefByTxid = await this.fetchTransactionBeefMap([txid])
144
+ return outputs.map(output => this.parseOutputRecord(output, true, beefByTxid.get(output.txid)))
100
145
  }
101
146
 
102
147
  async findUTXOsForTopic (topic: string, since?: number, limit?: number, includeBEEF: boolean = false): Promise<Output[]> {
@@ -113,45 +158,25 @@ export class KnexStorage implements Storage {
113
158
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
114
159
  query.orderBy('outputs.score', 'asc')
115
160
 
116
- // Select necessary fields from outputs and conditionally include beef from transactions
117
- const selectFields = [
118
- 'outputs.txid',
119
- 'outputs.outputIndex',
120
- 'outputs.outputScript',
121
- 'outputs.topic',
122
- 'outputs.satoshis',
123
- 'outputs.outputsConsumed',
124
- 'outputs.spent',
125
- 'outputs.consumedBy',
126
- 'outputs.score'
127
- ]
128
-
129
- if (includeBEEF) {
130
- // eslint-disable-next-line @typescript-eslint/no-floating-promises
131
- query.leftJoin('transactions', 'outputs.txid', 'transactions.txid')
132
- selectFields.push('transactions.beef')
133
- }
134
-
135
161
  // Apply limit if specified
136
162
  if (limit !== undefined && limit > 0) {
137
163
  // eslint-disable-next-line @typescript-eslint/no-floating-promises
138
164
  query.limit(limit)
139
165
  }
140
166
 
141
- const outputs = await query.select(selectFields)
167
+ const outputs = await query.select([...OUTPUT_SELECT_FIELDS])
142
168
 
143
169
  if (outputs === undefined || outputs.length === 0) {
144
170
  return []
145
171
  }
146
172
 
147
- return outputs.map(output => ({
148
- ...output,
149
- outputScript: [...output.outputScript],
150
- beef: includeBEEF ? (output.beef !== undefined ? [...output.beef] : undefined) : undefined,
151
- spent: Boolean(output.spent),
152
- outputsConsumed: JSON.parse(output.outputsConsumed),
153
- consumedBy: JSON.parse(output.consumedBy)
154
- }))
173
+ if (!includeBEEF) {
174
+ return outputs.map(output => this.parseOutputRecord(output, false))
175
+ }
176
+
177
+ const txids = Array.from(new Set(outputs.map(output => output.txid)))
178
+ const beefByTxid = await this.fetchTransactionBeefMap(txids)
179
+ return outputs.map(output => this.parseOutputRecord(output, true, beefByTxid.get(output.txid)))
155
180
  }
156
181
 
157
182
  async deleteOutput (txid: string, outputIndex: number, _: string): Promise<void> {