@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.
- package/README.md +106 -226
- package/dist/cjs/package.json +2 -2
- package/dist/cjs/src/Engine.js +56 -8
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/storage/knex/KnexStorage.js +80 -75
- package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/Engine.js +54 -7
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/storage/knex/KnexStorage.js +80 -75
- package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/Engine.d.ts +2 -0
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/storage/Storage.d.ts +10 -0
- package/dist/types/src/storage/Storage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/KnexStorage.d.ts +7 -0
- package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/docs/API.md +580 -72
- package/docs/examples/README.md +2 -2
- package/docs/examples/gs-wip.md +76 -68
- package/package.json +2 -2
- package/src/Engine.ts +74 -8
- package/src/__tests/Engine.test.ts +28 -0
- package/src/storage/Storage.ts +11 -0
- package/src/storage/knex/KnexStorage.ts +104 -79
package/docs/examples/README.md
CHANGED
|
@@ -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
|
|
11
|
+
- [Getting Started](./gs-wip.md) — Introduction and low-level engine setup guide
|
|
12
12
|
|
|
13
13
|
---
|
|
14
14
|
|
package/docs/examples/gs-wip.md
CHANGED
|
@@ -6,106 +6,114 @@
|
|
|
6
6
|
|
|
7
7
|
## Introduction to BSV Overlay Services Engine
|
|
8
8
|
|
|
9
|
-
The BSV Overlay Services Engine is
|
|
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
|
|
14
|
-
2. **Lookup Services
|
|
15
|
-
3. **Storage
|
|
16
|
-
4. **Chain Tracker
|
|
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
|
-
|
|
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
|
-
|
|
33
|
-
|
|
34
|
-
|
|
26
|
+
```bash
|
|
27
|
+
npm i @bsv/overlay @bsv/sdk knex
|
|
28
|
+
```
|
|
35
29
|
|
|
36
|
-
|
|
37
|
-
const chainTracker = new WoChain();
|
|
30
|
+
Create the engine with your concrete implementations:
|
|
38
31
|
|
|
39
|
-
|
|
40
|
-
|
|
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
|
-
|
|
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
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
}
|
|
78
|
+
const steak = await engine.submit({
|
|
79
|
+
beef: tx.toBEEF(),
|
|
80
|
+
topics: ['tm_example']
|
|
81
|
+
})
|
|
56
82
|
|
|
57
|
-
|
|
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
|
-
|
|
88
|
+
Ask a lookup service a domain-specific question:
|
|
68
89
|
|
|
69
90
|
```ts
|
|
70
|
-
const
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
###
|
|
101
|
+
### Service Documentation
|
|
86
102
|
|
|
87
|
-
|
|
103
|
+
Overlay clients and dashboards can retrieve service documentation and metadata directly from the engine:
|
|
88
104
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
105
|
+
```ts
|
|
106
|
+
const topicDocs = await engine.getDocumentationForTopicManger('tm_example')
|
|
107
|
+
const lookupDocs = await engine.getDocumentationForLookupServiceProvider('ls_example')
|
|
92
108
|
|
|
93
|
-
|
|
109
|
+
const topics = await engine.listTopicManagers()
|
|
110
|
+
const lookupServices = await engine.listLookupServiceProviders()
|
|
111
|
+
```
|
|
94
112
|
|
|
95
|
-
|
|
113
|
+
### Deployment Path
|
|
96
114
|
|
|
97
|
-
|
|
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
|
|
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.
|
|
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.
|
|
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 =
|
|
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
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
? candidateInput.sourceTXID
|
|
646
|
-
: candidateInput.sourceTransaction?.id('hex')
|
|
707
|
+
if (sourceTXID === undefined) {
|
|
708
|
+
return
|
|
709
|
+
}
|
|
647
710
|
|
|
648
|
-
|
|
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...?
|
package/src/storage/Storage.ts
CHANGED
|
@@ -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
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
|
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
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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(
|
|
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
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
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> {
|