@bsv/overlay 0.1.0-alpha.1 → 0.1.0-alpha.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/mod.js +5 -14
- package/dist/cjs/mod.js.map +1 -1
- package/dist/cjs/package.json +2 -2
- package/dist/cjs/src/Engine.js +110 -43
- package/dist/cjs/src/Engine.js.map +1 -1
- package/dist/cjs/src/storage/knex/KnexStorage.js +25 -3
- package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/mod.js +2 -13
- package/dist/esm/mod.js.map +1 -1
- package/dist/esm/src/Engine.js +110 -43
- package/dist/esm/src/Engine.js.map +1 -1
- package/dist/esm/src/storage/knex/KnexStorage.js +24 -3
- package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/mod.d.ts +12 -12
- package/dist/types/mod.d.ts.map +1 -1
- package/dist/types/src/AdmittanceInstructions.d.ts +2 -2
- package/dist/types/src/AdmittanceInstructions.d.ts.map +1 -1
- package/dist/types/src/Engine.d.ts +46 -26
- package/dist/types/src/Engine.d.ts.map +1 -1
- package/dist/types/src/LookupAnswer.d.ts.map +1 -1
- package/dist/types/src/LookupFormula.d.ts +5 -5
- package/dist/types/src/LookupFormula.d.ts.map +1 -1
- package/dist/types/src/LookupQuestion.d.ts.map +1 -1
- package/dist/types/src/LookupService.d.ts +32 -20
- package/dist/types/src/LookupService.d.ts.map +1 -1
- package/dist/types/src/Output.d.ts +4 -4
- package/dist/types/src/Output.d.ts.map +1 -1
- package/dist/types/src/STEAK.d.ts +1 -1
- package/dist/types/src/TopicManager.d.ts +5 -5
- package/dist/types/src/TopicManager.d.ts.map +1 -1
- package/dist/types/src/storage/Storage.d.ts +47 -34
- package/dist/types/src/storage/Storage.d.ts.map +1 -1
- package/dist/types/src/storage/knex/KnexStorage.d.ts +6 -4
- package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/mod.ts +12 -12
- package/package.json +2 -2
- package/src/AdmittanceInstructions.ts +8 -8
- package/src/Engine.ts +458 -383
- package/src/LookupAnswer.ts +7 -7
- package/src/LookupFormula.ts +21 -21
- package/src/LookupQuestion.ts +9 -9
- package/src/LookupService.ts +54 -36
- package/src/Output.ts +22 -22
- package/src/STEAK.ts +1 -1
- package/src/TopicManager.ts +21 -15
- package/src/__tests/Engine.test.ts +6 -4
- package/src/storage/Storage.ts +49 -34
- package/src/storage/knex/KnexStorage.ts +101 -76
package/src/Engine.ts
CHANGED
|
@@ -1,419 +1,494 @@
|
|
|
1
|
-
import TopicManager from
|
|
2
|
-
import LookupService from
|
|
3
|
-
import Storage from './storage/Storage.js'
|
|
4
|
-
import type { AdmittanceInstructions } from
|
|
1
|
+
import { TopicManager } from './TopicManager.js'
|
|
2
|
+
import { LookupService } from './LookupService.js'
|
|
3
|
+
import { Storage } from './storage/Storage.js'
|
|
4
|
+
import type { AdmittanceInstructions } from './AdmittanceInstructions.js'
|
|
5
5
|
import type { Output } from './Output.js'
|
|
6
|
-
import { TaggedBEEF } from
|
|
6
|
+
import { TaggedBEEF } from './TaggedBEEF.js'
|
|
7
7
|
import { STEAK } from './STEAK.js'
|
|
8
|
-
import { LookupQuestion } from
|
|
9
|
-
import { LookupAnswer } from
|
|
10
|
-
import { LookupFormula } from
|
|
11
|
-
import { Transaction, ChainTracker } from '@bsv/sdk'
|
|
8
|
+
import { LookupQuestion } from './LookupQuestion.js'
|
|
9
|
+
import { LookupAnswer } from './LookupAnswer.js'
|
|
10
|
+
import { LookupFormula } from './LookupFormula.js'
|
|
11
|
+
import { Transaction, ChainTracker, MerklePath, Broadcaster } from '@bsv/sdk'
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
14
|
* Am engine for running BSV Overlay Services (topic managers and lookup services).
|
|
15
15
|
*/
|
|
16
|
-
export
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
16
|
+
export class Engine {
|
|
17
|
+
/**
|
|
18
|
+
* Creates a new Overlay Services Engine
|
|
19
|
+
* @param {[key: string]: TopicManager} managers - manages topic admittance
|
|
20
|
+
* @param {[key: string]: LookupService} lookupServices - manages UTXO lookups
|
|
21
|
+
* @param {Storage} storage - for interacting with internally-managed persistent data
|
|
22
|
+
* @param {ChainTracker} chainTracker - Verifies SPV data associated with transactions
|
|
23
|
+
* @param {Broadcaster} [Broadcaster] - broadcaster used for broadcasting the incoming transaction
|
|
24
|
+
*/
|
|
25
|
+
constructor(
|
|
26
|
+
public managers: { [key: string]: TopicManager },
|
|
27
|
+
public lookupServices: { [key: string]: LookupService },
|
|
28
|
+
public storage: Storage,
|
|
29
|
+
public chainTracker: ChainTracker,
|
|
30
|
+
public broadcaster?: Broadcaster
|
|
31
|
+
) { }
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Submits a transaction for processing by Overlay Services.
|
|
35
|
+
* @param taggedBEEF — The transaction to process
|
|
36
|
+
* @returns The submitted transaction execution acknowledgement
|
|
37
|
+
*/
|
|
38
|
+
async submit(taggedBEEF: TaggedBEEF): Promise<STEAK> {
|
|
39
|
+
for (const t of taggedBEEF.topics) {
|
|
40
|
+
if (this.managers[t] === undefined || this.managers[t] === null) {
|
|
41
|
+
throw new Error(`This server does not support this topic: ${t}`)
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// Validate the transaction SPV information
|
|
45
|
+
const tx = Transaction.fromBEEF(taggedBEEF.beef)
|
|
46
|
+
const txid = tx.id('hex')
|
|
47
|
+
const txValid = await tx.verify(this.chainTracker)
|
|
48
|
+
if (!txValid) throw new Error('Unable to verify SPV information.')
|
|
49
|
+
|
|
50
|
+
// Find UTXOs belonging to a particular topic
|
|
51
|
+
const steak: STEAK = {}
|
|
52
|
+
for (const topic of taggedBEEF.topics) {
|
|
53
|
+
// Ensure transaction is not already applied to the topic
|
|
54
|
+
const dupeCheck = await this.storage.doesAppliedTransactionExist({
|
|
55
|
+
txid,
|
|
56
|
+
topic
|
|
57
|
+
})
|
|
58
|
+
if (dupeCheck) {
|
|
59
|
+
// The transaction was already processed.
|
|
60
|
+
// Currently, NO OUTPUTS ARE ADMITTED FOR DUPLICATE TRANSACTIONS.
|
|
61
|
+
// An alternative decision, one that was decided against, would be to act as if the operation was successful: looking up and returning the list of admitted outputs from when the transaction was originally processed.
|
|
62
|
+
// This was decided against, because we don't want to encourage unnecessary flooding of duplicative transactions to overlay services.
|
|
63
|
+
steak[topic] = {
|
|
64
|
+
outputsToAdmit: [],
|
|
65
|
+
coinsToRetain: []
|
|
41
66
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// Check if any input of this transaction is a previous UTXO, adding previous UTXOs to the list
|
|
69
|
-
const previousCoins: number[] = []
|
|
70
|
-
for (const [i, input] of tx.inputs.entries()) {
|
|
71
|
-
const previousTXID = input.sourceTXID || input.sourceTransaction?.id('hex') as string
|
|
72
|
-
// Check if a previous UTXO exists in the storage medium
|
|
73
|
-
const output = await this.storage.findOutput(
|
|
74
|
-
previousTXID,
|
|
75
|
-
input.sourceOutputIndex,
|
|
76
|
-
topic
|
|
77
|
-
)
|
|
78
|
-
if (output) {
|
|
79
|
-
previousCoins.push(i)
|
|
80
|
-
|
|
81
|
-
// This output is now spent.
|
|
82
|
-
await this.storage.markUTXOAsSpent(
|
|
83
|
-
output.txid,
|
|
84
|
-
output.outputIndex,
|
|
85
|
-
topic
|
|
86
|
-
)
|
|
87
|
-
|
|
88
|
-
// Notify the lookup services about the spending of this output
|
|
89
|
-
for (const l of Object.values(this.lookupServices)) {
|
|
90
|
-
try {
|
|
91
|
-
if (l.outputSpent) {
|
|
92
|
-
await l.outputSpent(
|
|
93
|
-
output.txid,
|
|
94
|
-
output.outputIndex,
|
|
95
|
-
topic
|
|
96
|
-
)
|
|
97
|
-
}
|
|
98
|
-
} catch (_) { }
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
// Use the manager to determine which outputs are admissable
|
|
104
|
-
let admissableOutputs: AdmittanceInstructions
|
|
67
|
+
continue
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Check if any input of this transaction is a previous UTXO, adding previous UTXOs to the list
|
|
71
|
+
const previousCoins: number[] = []
|
|
72
|
+
for (const [i, input] of tx.inputs.entries()) {
|
|
73
|
+
const previousTXID = input.sourceTXID || input.sourceTransaction?.id('hex') as string
|
|
74
|
+
// Check if a previous UTXO exists in the storage medium
|
|
75
|
+
const output = await this.storage.findOutput(
|
|
76
|
+
previousTXID,
|
|
77
|
+
input.sourceOutputIndex,
|
|
78
|
+
topic
|
|
79
|
+
)
|
|
80
|
+
if (output !== undefined && output !== null) {
|
|
81
|
+
previousCoins.push(i)
|
|
82
|
+
|
|
83
|
+
// This output is now spent.
|
|
84
|
+
await this.storage.markUTXOAsSpent(
|
|
85
|
+
output.txid,
|
|
86
|
+
output.outputIndex,
|
|
87
|
+
topic
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
// Notify the lookup services about the spending of this output
|
|
91
|
+
for (const l of Object.values(this.lookupServices)) {
|
|
105
92
|
try {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
93
|
+
if (l.outputSpent !== undefined && l.outputSpent !== null) {
|
|
94
|
+
await l.outputSpent(
|
|
95
|
+
output.txid,
|
|
96
|
+
output.outputIndex,
|
|
97
|
+
topic
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
} catch (_) { }
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Use the manager to determine which outputs are admissable
|
|
106
|
+
let admissableOutputs: AdmittanceInstructions
|
|
107
|
+
try {
|
|
108
|
+
admissableOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins)
|
|
109
|
+
} catch (_) {
|
|
110
|
+
// If the topic manager throws an error, other topics may still succeed, so we continue to the next one.
|
|
111
|
+
// No outputs were admitted to this topic in this case. Note, however, that the transaction is still valid according to Bitcoin, so it may have spent some previous overlay members. This is unavoidable and good.
|
|
112
|
+
steak[topic] = {
|
|
113
|
+
outputsToAdmit: [],
|
|
114
|
+
coinsToRetain: []
|
|
115
|
+
}
|
|
116
|
+
continue
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Keep track of which outputs to admit, mark as stale, or retain
|
|
120
|
+
const outputsToAdmit: number[] = admissableOutputs.outputsToAdmit
|
|
121
|
+
const staleCoins: Array<{
|
|
122
|
+
txid: string
|
|
123
|
+
outputIndex: number
|
|
124
|
+
}> = []
|
|
125
|
+
const outputsConsumed: Array<{
|
|
126
|
+
txid: string
|
|
127
|
+
outputIndex: number
|
|
128
|
+
}> = []
|
|
129
|
+
|
|
130
|
+
// Find which outputs should not be retained and mark them as stale
|
|
131
|
+
// For each of the previous UTXOs, if the the UTXO was not included in the list of UTXOs identified for retention, then it will be marked as stale.
|
|
132
|
+
for (const inputIndex of previousCoins) {
|
|
133
|
+
const previousTXID = tx.inputs[inputIndex].sourceTXID || tx.inputs[inputIndex].sourceTransaction?.id('hex') as string
|
|
134
|
+
const previousOutputIndex = tx.inputs[inputIndex].sourceOutputIndex
|
|
135
|
+
if (!admissableOutputs.coinsToRetain.includes(inputIndex)) {
|
|
136
|
+
staleCoins.push({
|
|
137
|
+
txid: previousTXID,
|
|
138
|
+
outputIndex: previousOutputIndex
|
|
139
|
+
})
|
|
140
|
+
} else {
|
|
141
|
+
outputsConsumed.push({
|
|
142
|
+
txid: previousTXID,
|
|
143
|
+
outputIndex: previousOutputIndex
|
|
144
|
+
})
|
|
145
|
+
}
|
|
146
|
+
}
|
|
145
147
|
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
148
|
+
// Remove stale outputs recursively
|
|
149
|
+
for (const coin of staleCoins) {
|
|
150
|
+
const output = await this.storage.findOutput(coin.txid, coin.outputIndex, topic)
|
|
151
|
+
if (output !== undefined && output !== null) {
|
|
152
|
+
await this.deleteUTXODeep(output)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle admittance and notification of incoming UTXOs
|
|
157
|
+
const newUTXOs: Array<{ txid: string, outputIndex: number }> = []
|
|
158
|
+
for (const outputIndex of outputsToAdmit) {
|
|
159
|
+
// Store the output
|
|
160
|
+
await this.storage.insertOutput({
|
|
161
|
+
txid,
|
|
162
|
+
outputIndex,
|
|
163
|
+
outputScript: tx.outputs[outputIndex].lockingScript.toBinary(),
|
|
164
|
+
satoshis: tx.outputs[outputIndex].satoshis as number,
|
|
165
|
+
topic,
|
|
166
|
+
spent: false,
|
|
167
|
+
beef: taggedBEEF.beef,
|
|
168
|
+
consumedBy: [],
|
|
169
|
+
outputsConsumed
|
|
170
|
+
})
|
|
171
|
+
newUTXOs.push({
|
|
172
|
+
txid,
|
|
173
|
+
outputIndex
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
// Notify all the lookup services about the new UTXO
|
|
177
|
+
for (const l of Object.values(this.lookupServices)) {
|
|
178
|
+
try {
|
|
179
|
+
if (l.outputAdded !== undefined && l.outputAdded !== null) {
|
|
180
|
+
await l.outputAdded(txid, outputIndex, tx.outputs[outputIndex].lockingScript, topic)
|
|
152
181
|
}
|
|
182
|
+
} catch (_) { }
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Update each output consumed to know who consumed it
|
|
187
|
+
for (const output of outputsConsumed) {
|
|
188
|
+
const outputToUpdate = await this.storage.findOutput(output.txid, output.outputIndex, topic)
|
|
189
|
+
if (outputToUpdate !== undefined && outputToUpdate !== null) {
|
|
190
|
+
const newConsumedBy = [...new Set([...newUTXOs, ...outputToUpdate.consumedBy])]
|
|
191
|
+
// Note: only update if newConsumedBy !== new Set(JSON.parse(outputToUpdate.consumedBy)) ?
|
|
192
|
+
await this.storage.updateConsumedBy(output.txid, output.outputIndex, topic, newConsumedBy)
|
|
193
|
+
}
|
|
194
|
+
}
|
|
153
195
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
txid,
|
|
160
|
-
outputIndex,
|
|
161
|
-
outputScript: tx.outputs[outputIndex].lockingScript.toBinary(),
|
|
162
|
-
satoshis: tx.outputs[outputIndex].satoshis as number,
|
|
163
|
-
topic,
|
|
164
|
-
spent: false,
|
|
165
|
-
beef: taggedBEEF.beef,
|
|
166
|
-
consumedBy: [],
|
|
167
|
-
outputsConsumed
|
|
168
|
-
})
|
|
169
|
-
newUTXOs.push({
|
|
170
|
-
txid,
|
|
171
|
-
outputIndex
|
|
172
|
-
})
|
|
173
|
-
|
|
174
|
-
// Notify all the lookup services about the new UTXO
|
|
175
|
-
for (const l of Object.values(this.lookupServices)) {
|
|
176
|
-
try {
|
|
177
|
-
if (l.outputAdded) {
|
|
178
|
-
await l.outputAdded(txid, outputIndex, tx.outputs[outputIndex].lockingScript, topic)
|
|
179
|
-
}
|
|
180
|
-
} catch (_) { }
|
|
181
|
-
}
|
|
182
|
-
}
|
|
196
|
+
// Insert the applied transaction to prevent duplicate processing
|
|
197
|
+
await this.storage.insertAppliedTransaction({
|
|
198
|
+
txid,
|
|
199
|
+
topic
|
|
200
|
+
})
|
|
183
201
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
if (outputToUpdate) {
|
|
188
|
-
const newConsumedBy = [...new Set([...newUTXOs, ...outputToUpdate.consumedBy])]
|
|
189
|
-
// Note: only update if newConsumedBy !== new Set(JSON.parse(outputToUpdate.consumedBy)) ?
|
|
190
|
-
await this.storage.updateConsumedBy(output.txid, output.outputIndex, topic, newConsumedBy)
|
|
191
|
-
}
|
|
192
|
-
}
|
|
202
|
+
// Keep track of what outputs were admitted for what topic
|
|
203
|
+
steak[topic] = admissableOutputs
|
|
204
|
+
}
|
|
193
205
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
206
|
+
// Broadcast the transaction
|
|
207
|
+
if (Object.keys(steak).length > 0 && this.broadcaster !== undefined) {
|
|
208
|
+
await this.broadcaster.broadcast(tx)
|
|
209
|
+
}
|
|
210
|
+
return steak
|
|
211
|
+
// TODO propagate transaction to other nodes according to synchronization agreements
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Submit a lookup question to the Overlay Services Engine, and receive bakc a Lookup Answer
|
|
216
|
+
* @param LookupQuestion — The question to ask the Overlay Services Engine
|
|
217
|
+
* @returns The answer to the question
|
|
218
|
+
*/
|
|
219
|
+
async lookup(lookupQuestion: LookupQuestion): Promise<LookupAnswer> {
|
|
220
|
+
// Validate a lookup service for the provider is found
|
|
221
|
+
const lookupService = this.lookupServices[lookupQuestion.service]
|
|
222
|
+
if (lookupService === undefined || lookupService === null) throw new Error(`Lookup service not found for provider: ${lookupQuestion.service}`)
|
|
223
|
+
|
|
224
|
+
let lookupResult = await lookupService.lookup(lookupQuestion)
|
|
225
|
+
// Handle custom lookup service answers
|
|
226
|
+
if ((lookupResult as LookupAnswer).type === 'freeform' || (lookupResult as LookupAnswer).type === 'output-list') {
|
|
227
|
+
return lookupResult as LookupAnswer
|
|
228
|
+
}
|
|
229
|
+
lookupResult = lookupResult as LookupFormula
|
|
230
|
+
|
|
231
|
+
const hydratedOutputs: Array<{ beef: number[], outputIndex: number }> = []
|
|
232
|
+
|
|
233
|
+
for (const { txid, outputIndex, history } of lookupResult) {
|
|
234
|
+
// Make sure this is an unspent output (UTXO)
|
|
235
|
+
const UTXO = await this.storage.findOutput(
|
|
236
|
+
txid,
|
|
237
|
+
outputIndex,
|
|
238
|
+
undefined,
|
|
239
|
+
false
|
|
240
|
+
)
|
|
241
|
+
if (UTXO === undefined || UTXO === null) continue
|
|
242
|
+
|
|
243
|
+
// Get the history for this utxo and construct a BRC-8 Envelope
|
|
244
|
+
const output = await this.getUTXOHistory(UTXO, history, 0)
|
|
245
|
+
if (output !== undefined && output !== null) {
|
|
246
|
+
hydratedOutputs.push({
|
|
247
|
+
beef: output.beef,
|
|
248
|
+
outputIndex: output.outputIndex
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return {
|
|
253
|
+
type: 'output-list',
|
|
254
|
+
outputs: hydratedOutputs
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Traverse and return the history of a UTXO.
|
|
260
|
+
*
|
|
261
|
+
* This method traverses the history of a given Unspent Transaction Output (UTXO) and returns
|
|
262
|
+
* its historical data based on the provided history selector and current depth.
|
|
263
|
+
*
|
|
264
|
+
* @param output - The UTXO to traverse the history for.
|
|
265
|
+
* @param historySelector - Optionally directs the history traversal:
|
|
266
|
+
* - If a number, denotes how many previous spends (in terms of chain depth) to include.
|
|
267
|
+
* - If a function, accepts a BEEF-formatted transaction, an output index, and the current depth as parameters,
|
|
268
|
+
* returning a promise that resolves to a boolean indicating whether to include the output in the history.
|
|
269
|
+
* @param {number} [currentDepth=0] - The current depth of the traversal relative to the top-level UTXO.
|
|
270
|
+
*
|
|
271
|
+
* @returns {Promise<Output | undefined>} - A promise that resolves to the output history if found, or undefined if not.
|
|
272
|
+
*/
|
|
273
|
+
async getUTXOHistory(
|
|
274
|
+
output: Output,
|
|
275
|
+
historySelector?: ((beef: number[], outputIndex: number, currentDepth: number) => Promise<boolean>) | number,
|
|
276
|
+
currentDepth = 0
|
|
277
|
+
): Promise<Output | undefined> {
|
|
278
|
+
// If we have an output but no history selector, jsut return the output.
|
|
279
|
+
if (typeof historySelector === 'undefined') {
|
|
280
|
+
return output
|
|
281
|
+
}
|
|
199
282
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
283
|
+
// Determine if history traversal should continue for the current node
|
|
284
|
+
let shouldTraverseHistory
|
|
285
|
+
if (typeof historySelector !== 'number') {
|
|
286
|
+
shouldTraverseHistory = await historySelector(output.beef, output.outputIndex, currentDepth)
|
|
287
|
+
} else {
|
|
288
|
+
shouldTraverseHistory = currentDepth <= historySelector
|
|
289
|
+
}
|
|
203
290
|
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
291
|
+
if (shouldTraverseHistory === false) {
|
|
292
|
+
return undefined
|
|
293
|
+
} else if (output !== null && output !== undefined && output.outputsConsumed.length === 0) {
|
|
294
|
+
return output
|
|
207
295
|
}
|
|
208
296
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
*/
|
|
214
|
-
async lookup(lookupQuestion: LookupQuestion): Promise<LookupAnswer> {
|
|
215
|
-
// Validate a lookup service for the provider is found
|
|
216
|
-
const lookupService = this.lookupServices[lookupQuestion.service]
|
|
217
|
-
if (!lookupService) throw new Error(`Lookup service not found for provider: ${lookupQuestion.service}`)
|
|
218
|
-
|
|
219
|
-
let lookupResult = await lookupService.lookup(lookupQuestion)
|
|
220
|
-
// Handle custom lookup service answers
|
|
221
|
-
if ((lookupResult as LookupAnswer).type === 'freeform' || (lookupResult as LookupAnswer).type === 'output-list') {
|
|
222
|
-
return lookupResult as LookupAnswer
|
|
223
|
-
}
|
|
224
|
-
lookupResult = lookupResult as LookupFormula
|
|
297
|
+
try {
|
|
298
|
+
// Query the storage engine for UTXOs consumed by this UTXO
|
|
299
|
+
// Only retrieve unique values in case outputs are doubly referenced
|
|
300
|
+
const outputsConsumed: Array<{ txid: string, outputIndex: number }> = output.outputsConsumed
|
|
225
301
|
|
|
226
|
-
|
|
302
|
+
// Find the child outputs for each utxo consumed by the current output
|
|
303
|
+
const childHistories = (await Promise.all(
|
|
304
|
+
outputsConsumed.map(async (outputIdentifier) => {
|
|
305
|
+
const output = await this.storage.findOutput(outputIdentifier.txid, outputIdentifier.outputIndex)
|
|
227
306
|
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
307
|
+
// Make sure an output was found
|
|
308
|
+
if (output === undefined || output === null) {
|
|
309
|
+
return undefined
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Find previousUTXO history
|
|
313
|
+
return await this.getUTXOHistory(output, historySelector, currentDepth + 1)
|
|
314
|
+
})
|
|
315
|
+
)).filter(x => x !== undefined)
|
|
316
|
+
|
|
317
|
+
const tx = Transaction.fromBEEF(output.beef)
|
|
318
|
+
for (const input of childHistories) {
|
|
319
|
+
if (input === undefined || input === null) continue
|
|
320
|
+
const inputIndex = tx.inputs.findIndex((input) => {
|
|
321
|
+
const sourceTXID = input.sourceTXID !== undefined && input.sourceTXID !== ''
|
|
322
|
+
? input.sourceTXID
|
|
323
|
+
: input.sourceTransaction?.id('hex')
|
|
324
|
+
return sourceTXID === output.txid && input.sourceOutputIndex === output.outputIndex
|
|
325
|
+
})
|
|
326
|
+
tx.inputs[inputIndex].sourceTransaction = Transaction.fromBEEF(input.beef)
|
|
327
|
+
}
|
|
328
|
+
const beef = tx.toBEEF()
|
|
329
|
+
return {
|
|
330
|
+
...output,
|
|
331
|
+
beef
|
|
332
|
+
}
|
|
333
|
+
} catch (e) {
|
|
334
|
+
// Handle any errors that occurred
|
|
335
|
+
// Note: Test this!
|
|
336
|
+
console.error(`Error retrieving UTXO history: ${e}`)
|
|
337
|
+
// return []
|
|
338
|
+
throw new Error(`Error retrieving UTXO history: ${e}`)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Delete a UTXO and all stale consumed inputs.
|
|
344
|
+
* @param output - The UTXO to be deleted.
|
|
345
|
+
* @returns {Promise<void>} - A promise that resolves when the deletion process is complete.
|
|
346
|
+
*/
|
|
347
|
+
private async deleteUTXODeep(output: Output): Promise<void> {
|
|
348
|
+
try {
|
|
349
|
+
// Delete the current output IFF there are no references to it
|
|
350
|
+
if (output.consumedBy.length === 0) {
|
|
351
|
+
await this.storage.deleteOutput(output.txid, output.outputIndex, output.topic)
|
|
352
|
+
|
|
353
|
+
// Notify the lookup services of the UTXO being deleted
|
|
354
|
+
for (const l of Object.values(this.lookupServices)) {
|
|
355
|
+
try {
|
|
356
|
+
await l.outputDeleted?.(
|
|
357
|
+
output.txid,
|
|
358
|
+
output.outputIndex,
|
|
359
|
+
output.topic
|
|
235
360
|
)
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
// Get the history for this utxo and construct a BRC-8 Envelope
|
|
239
|
-
const output = await this.getUTXOHistory(UTXO, history, 0)
|
|
240
|
-
if (output) {
|
|
241
|
-
hydratedOutputs.push({
|
|
242
|
-
beef: output.beef,
|
|
243
|
-
outputIndex: output.outputIndex
|
|
244
|
-
})
|
|
245
|
-
}
|
|
246
|
-
}
|
|
247
|
-
return {
|
|
248
|
-
type: 'output-list',
|
|
249
|
-
outputs: hydratedOutputs
|
|
361
|
+
} catch (_) { }
|
|
250
362
|
}
|
|
251
|
-
|
|
363
|
+
}
|
|
252
364
|
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
* @param output
|
|
258
|
-
* @param txid
|
|
259
|
-
* @param outputIndex
|
|
260
|
-
* @param id
|
|
261
|
-
* @returns {Promise<EnvelopeEvidenceApi>}
|
|
262
|
-
*/
|
|
263
|
-
async getUTXOHistory(
|
|
264
|
-
output: Output,
|
|
265
|
-
historySelector?: ((beef: number[], outputIndex: number, currentDepth: number) => Promise<boolean>) | number, currentDepth = 0,
|
|
266
|
-
): Promise<Output | undefined> {
|
|
267
|
-
// If we have an output but no history selector, jsut return the output.
|
|
268
|
-
if (typeof historySelector === 'undefined') {
|
|
269
|
-
return output
|
|
270
|
-
}
|
|
365
|
+
// If there are no more consumed utxos, return
|
|
366
|
+
if (output.outputsConsumed.length === 0) {
|
|
367
|
+
return
|
|
368
|
+
}
|
|
271
369
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
shouldTraverseHistory = await historySelector(output.beef, output.outputIndex, currentDepth)
|
|
276
|
-
} else {
|
|
277
|
-
shouldTraverseHistory = currentDepth <= historySelector
|
|
278
|
-
}
|
|
370
|
+
// Delete any stale outputs that were consumed as inputs
|
|
371
|
+
output.outputsConsumed.map(async (outputIdentifier) => {
|
|
372
|
+
const staleOutput = await this.storage.findOutput(outputIdentifier.txid, outputIdentifier.outputIndex, output.topic)
|
|
279
373
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
return output
|
|
374
|
+
// Make sure an output was found
|
|
375
|
+
if (staleOutput === null || staleOutput === undefined) {
|
|
376
|
+
return undefined
|
|
284
377
|
}
|
|
285
378
|
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
// Find the child outputs for each utxo consumed by the current output
|
|
292
|
-
const childHistories = await (await Promise.all(
|
|
293
|
-
outputsConsumed.map(async (outputIdentifier) => {
|
|
294
|
-
const output = await this.storage.findOutput(outputIdentifier.txid, outputIdentifier.outputIndex)
|
|
295
|
-
|
|
296
|
-
// Make sure an output was found
|
|
297
|
-
if (!output) {
|
|
298
|
-
return undefined
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Find previousUTXO history
|
|
302
|
-
return this.getUTXOHistory(output, historySelector, currentDepth + 1)
|
|
303
|
-
})
|
|
304
|
-
)).filter(x => x !== undefined)
|
|
305
|
-
|
|
306
|
-
const tx = Transaction.fromBEEF(output.beef)
|
|
307
|
-
for (const input of childHistories) {
|
|
308
|
-
if (!input) continue
|
|
309
|
-
const inputIndex = tx.inputs.findIndex((input) => {
|
|
310
|
-
const sourceTXID = input.sourceTXID || input.sourceTransaction?.id('hex') as string
|
|
311
|
-
return sourceTXID === output.txid && input.sourceOutputIndex === output.outputIndex
|
|
312
|
-
})
|
|
313
|
-
tx.inputs[inputIndex].sourceTransaction = Transaction.fromBEEF(input.beef)
|
|
314
|
-
}
|
|
315
|
-
const beef = tx.toBEEF()
|
|
316
|
-
return {
|
|
317
|
-
...output,
|
|
318
|
-
beef
|
|
319
|
-
}
|
|
320
|
-
} catch (e) {
|
|
321
|
-
// Handle any errors that occurred
|
|
322
|
-
// Note: Test this!
|
|
323
|
-
console.error(`Error retrieving UTXO history: ${e}`)
|
|
324
|
-
// return []
|
|
325
|
-
throw new Error(`Error retrieving UTXO history: ${e}`)
|
|
379
|
+
// Parse out the existing data, then concat the new outputs with no duplicates
|
|
380
|
+
if (staleOutput.consumedBy.length !== 0) {
|
|
381
|
+
staleOutput.consumedBy = staleOutput.consumedBy.filter(x => x.txid !== output.txid && x.outputIndex !== output.outputIndex)
|
|
382
|
+
// Update with the new consumedBy data
|
|
383
|
+
await this.storage.updateConsumedBy(outputIdentifier.txid, outputIdentifier.outputIndex, output.topic, staleOutput.consumedBy)
|
|
326
384
|
}
|
|
327
|
-
}
|
|
328
|
-
|
|
329
|
-
/**
|
|
330
|
-
* Delete a UTXO and all stale consumed inputs
|
|
331
|
-
* @param output
|
|
332
|
-
* @param id
|
|
333
|
-
* @param txid
|
|
334
|
-
* @param outputIndex
|
|
335
|
-
* @returns {Promise<void>}
|
|
336
|
-
*/
|
|
337
|
-
private async deleteUTXODeep(output: Output): Promise<void> {
|
|
338
|
-
try {
|
|
339
|
-
// Delete the current output IFF there are no references to it
|
|
340
|
-
if (output.consumedBy.length === 0) {
|
|
341
|
-
await this.storage.deleteOutput(output.txid, output.outputIndex, output.topic)
|
|
342
|
-
|
|
343
|
-
// Notify the lookup services of the UTXO being deleted
|
|
344
|
-
for (const l of Object.values(this.lookupServices)) {
|
|
345
|
-
try {
|
|
346
|
-
await l.outputDeleted?.(
|
|
347
|
-
output.txid,
|
|
348
|
-
output.outputIndex,
|
|
349
|
-
output.topic!
|
|
350
|
-
)
|
|
351
|
-
} catch (_) { }
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
385
|
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
386
|
+
// Find previousUTXO history
|
|
387
|
+
return await this.deleteUTXODeep(staleOutput)
|
|
388
|
+
})
|
|
389
|
+
} catch (error) {
|
|
390
|
+
throw new Error(`Failed to delete all stale outputs: ${error as string}`)
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Recursively updates the Merkle proof for the given output and its consumedBy outputs.
|
|
396
|
+
* If the output matches the source transaction ID, its Merkle proof is updated directly.
|
|
397
|
+
* Otherwise, the Merkle proof is updated for the corresponding input in each transaction.
|
|
398
|
+
*
|
|
399
|
+
* @param output - The output to update with the new Merkle proof.
|
|
400
|
+
* @param proof - The Merkle proof to be applied to the output or its inputs.
|
|
401
|
+
* @param sourceTxid - The transaction ID of the source output whose Merkle proof is being updated.
|
|
402
|
+
*/
|
|
403
|
+
private async updateMerkleProof(output: Output, proof: MerklePath, recursionPath: Array<{ txid: string, outputIndex: number }>): Promise<void> {
|
|
404
|
+
// Add current output to recursionPath
|
|
405
|
+
recursionPath.push({ txid: output.txid, outputIndex: output.outputIndex })
|
|
406
|
+
|
|
407
|
+
const tx = Transaction.fromBEEF(output.beef)
|
|
408
|
+
|
|
409
|
+
// Handle the base case
|
|
410
|
+
if (output.txid === recursionPath[0].txid) {
|
|
411
|
+
tx.merklePath = proof
|
|
412
|
+
} else {
|
|
413
|
+
// Traverse inputs to update the Merkle proof according to the recursionPath
|
|
414
|
+
let currentInputs = tx.inputs
|
|
415
|
+
|
|
416
|
+
for (let i = recursionPath.length - 1; i >= 0; i--) {
|
|
417
|
+
const crumb = recursionPath[i]
|
|
418
|
+
|
|
419
|
+
for (const input of currentInputs) {
|
|
420
|
+
if (input.sourceTXID === crumb.txid && input.sourceOutputIndex === crumb.outputIndex) {
|
|
421
|
+
if (i === 0 && input.sourceTransaction !== undefined) {
|
|
422
|
+
input.sourceTransaction.merklePath = proof
|
|
423
|
+
} else if (input.sourceTransaction !== undefined) {
|
|
424
|
+
currentInputs = input.sourceTransaction.inputs
|
|
425
|
+
break
|
|
358
426
|
}
|
|
359
|
-
|
|
360
|
-
// Delete any stale outputs that were consumed as inputs
|
|
361
|
-
output.outputsConsumed.map(async (outputIdentifier) => {
|
|
362
|
-
const staleOutput = await this.storage.findOutput(outputIdentifier.txid, outputIdentifier.outputIndex, output.topic)
|
|
363
|
-
|
|
364
|
-
// Make sure an output was found
|
|
365
|
-
if (!staleOutput) {
|
|
366
|
-
return undefined
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Parse out the existing data, then concat the new outputs with no duplicates
|
|
370
|
-
if (staleOutput.consumedBy.length !== 0) {
|
|
371
|
-
staleOutput.consumedBy = staleOutput.consumedBy.filter(x => x.txid !== output.txid && x.outputIndex !== output.outputIndex)
|
|
372
|
-
// Update with the new consumedBy data
|
|
373
|
-
await this.storage.updateConsumedBy(outputIdentifier.txid, outputIdentifier.outputIndex, output.topic, staleOutput.consumedBy)
|
|
374
|
-
}
|
|
375
|
-
|
|
376
|
-
// Find previousUTXO history
|
|
377
|
-
return await this.deleteUTXODeep(staleOutput)
|
|
378
|
-
})
|
|
379
|
-
} catch (error) {
|
|
380
|
-
throw new Error(`Failed to delete all stale outputs: ${error}`)
|
|
427
|
+
}
|
|
381
428
|
}
|
|
429
|
+
}
|
|
382
430
|
}
|
|
383
431
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
* @public
|
|
387
|
-
* @returns {Promise<string[]>} - array of supported topic managers
|
|
388
|
-
*/
|
|
389
|
-
async listTopicManagers(): Promise<string[]> {
|
|
390
|
-
return Object.keys(this.managers)
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
/**
|
|
394
|
-
* Find a list of supported lookup services
|
|
395
|
-
* @public
|
|
396
|
-
* @returns {Promise<string[]>} - array of supported lookup services
|
|
397
|
-
*/
|
|
398
|
-
async listLookupServiceProviders(): Promise<string[]> {
|
|
399
|
-
return Object.keys(this.lookupServices)
|
|
400
|
-
}
|
|
432
|
+
// Update the output's BEEF in the storage DB
|
|
433
|
+
await this.storage.updateOutputBeef(output.txid, output.outputIndex, output.topic, tx.toBEEF())
|
|
401
434
|
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
return this.managers[manager].getDocumentation?.() || 'No documentation found!'
|
|
435
|
+
// Recursively update the consumedBy outputs
|
|
436
|
+
for (const consumingOutput of output.consumedBy) {
|
|
437
|
+
const consumedOutputs = await this.storage.findOutputsForTransaction(consumingOutput.txid)
|
|
438
|
+
for (const consumedOutput of consumedOutputs) {
|
|
439
|
+
await this.updateMerkleProof(consumedOutput, proof, [])
|
|
440
|
+
}
|
|
409
441
|
}
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Recursively prune UTXOs when an incoming Merkle Proof is received.
|
|
446
|
+
*
|
|
447
|
+
* @param txid - Transaction ID of the associated outputs to prune.
|
|
448
|
+
* @param proof - Merkle proof containing the Merkle path and other relevant data to verify the transaction.
|
|
449
|
+
*/
|
|
450
|
+
async handleNewMerkleProof(txid: string, proof: MerklePath): Promise<void> {
|
|
451
|
+
const outputs = await this.storage.findOutputsForTransaction(txid)
|
|
452
|
+
for (const output of outputs) {
|
|
453
|
+
await this.updateMerkleProof(output, proof, [])
|
|
418
454
|
}
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Find a list of supported topic managers
|
|
459
|
+
* @public
|
|
460
|
+
* @returns {Promise<string[]>} - array of supported topic managers
|
|
461
|
+
*/
|
|
462
|
+
async listTopicManagers(): Promise<string[]> {
|
|
463
|
+
return Object.keys(this.managers)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
/**
|
|
467
|
+
* Find a list of supported lookup services
|
|
468
|
+
* @public
|
|
469
|
+
* @returns {Promise<string[]>} - array of supported lookup services
|
|
470
|
+
*/
|
|
471
|
+
async listLookupServiceProviders(): Promise<string[]> {
|
|
472
|
+
return Object.keys(this.lookupServices)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Run a query to get the documentation for a particular topic manager
|
|
477
|
+
* @public
|
|
478
|
+
* @returns {Promise<string>} - the documentation for the topic manager
|
|
479
|
+
*/
|
|
480
|
+
async getDocumentationForTopicManager(manager: any): Promise<string> {
|
|
481
|
+
const documentation = await this.managers[manager]?.getDocumentation?.()
|
|
482
|
+
return documentation !== undefined ? documentation : 'No documentation found!'
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Run a query to get the documentation for a particular lookup service
|
|
487
|
+
* @public
|
|
488
|
+
* @returns {Promise<string>} - the documentation for the lookup service
|
|
489
|
+
*/
|
|
490
|
+
async getDocumentationForLookupServiceProvider(provider: any): Promise<string> {
|
|
491
|
+
const documentation = await this.lookupServices[provider]?.getDocumentation?.()
|
|
492
|
+
return documentation !== undefined ? documentation : 'No documentation found!'
|
|
493
|
+
}
|
|
419
494
|
}
|