@bsv/overlay 0.1.0-alpha.1 → 0.1.0-alpha.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.
Files changed (69) hide show
  1. package/dist/cjs/mod.js +5 -14
  2. package/dist/cjs/mod.js.map +1 -1
  3. package/dist/cjs/package.json +2 -2
  4. package/dist/cjs/src/Advertiser.js +3 -0
  5. package/dist/cjs/src/Advertiser.js.map +1 -0
  6. package/dist/cjs/src/Engine.js +110 -43
  7. package/dist/cjs/src/Engine.js.map +1 -1
  8. package/dist/cjs/src/SHIPAdvertisement.js +3 -0
  9. package/dist/cjs/src/SHIPAdvertisement.js.map +1 -0
  10. package/dist/cjs/src/SLAPAdvertisement.js +3 -0
  11. package/dist/cjs/src/SLAPAdvertisement.js.map +1 -0
  12. package/dist/cjs/src/storage/knex/KnexStorage.js +25 -3
  13. package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
  14. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  15. package/dist/esm/mod.js +2 -13
  16. package/dist/esm/mod.js.map +1 -1
  17. package/dist/esm/src/Advertiser.js +2 -0
  18. package/dist/esm/src/Advertiser.js.map +1 -0
  19. package/dist/esm/src/Engine.js +110 -43
  20. package/dist/esm/src/Engine.js.map +1 -1
  21. package/dist/esm/src/SHIPAdvertisement.js +2 -0
  22. package/dist/esm/src/SHIPAdvertisement.js.map +1 -0
  23. package/dist/esm/src/SLAPAdvertisement.js +2 -0
  24. package/dist/esm/src/SLAPAdvertisement.js.map +1 -0
  25. package/dist/esm/src/storage/knex/KnexStorage.js +24 -3
  26. package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
  27. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  28. package/dist/types/mod.d.ts +12 -12
  29. package/dist/types/mod.d.ts.map +1 -1
  30. package/dist/types/src/AdmittanceInstructions.d.ts +2 -2
  31. package/dist/types/src/AdmittanceInstructions.d.ts.map +1 -1
  32. package/dist/types/src/Advertiser.d.ts +47 -0
  33. package/dist/types/src/Advertiser.d.ts.map +1 -0
  34. package/dist/types/src/Engine.d.ts +46 -26
  35. package/dist/types/src/Engine.d.ts.map +1 -1
  36. package/dist/types/src/LookupAnswer.d.ts.map +1 -1
  37. package/dist/types/src/LookupFormula.d.ts +5 -5
  38. package/dist/types/src/LookupFormula.d.ts.map +1 -1
  39. package/dist/types/src/LookupQuestion.d.ts.map +1 -1
  40. package/dist/types/src/LookupService.d.ts +32 -20
  41. package/dist/types/src/LookupService.d.ts.map +1 -1
  42. package/dist/types/src/Output.d.ts +4 -4
  43. package/dist/types/src/Output.d.ts.map +1 -1
  44. package/dist/types/src/SHIPAdvertisement.d.ts +9 -0
  45. package/dist/types/src/SHIPAdvertisement.d.ts.map +1 -0
  46. package/dist/types/src/SLAPAdvertisement.d.ts +9 -0
  47. package/dist/types/src/SLAPAdvertisement.d.ts.map +1 -0
  48. package/dist/types/src/STEAK.d.ts +1 -1
  49. package/dist/types/src/TopicManager.d.ts +5 -5
  50. package/dist/types/src/TopicManager.d.ts.map +1 -1
  51. package/dist/types/src/storage/Storage.d.ts +48 -36
  52. package/dist/types/src/storage/Storage.d.ts.map +1 -1
  53. package/dist/types/src/storage/knex/KnexStorage.d.ts +6 -4
  54. package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
  55. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  56. package/mod.ts +12 -12
  57. package/package.json +2 -2
  58. package/src/AdmittanceInstructions.ts +8 -8
  59. package/src/Engine.ts +458 -383
  60. package/src/LookupAnswer.ts +7 -7
  61. package/src/LookupFormula.ts +21 -21
  62. package/src/LookupQuestion.ts +9 -9
  63. package/src/LookupService.ts +54 -36
  64. package/src/Output.ts +22 -22
  65. package/src/STEAK.ts +1 -1
  66. package/src/TopicManager.ts +21 -15
  67. package/src/__tests/Engine.test.ts +6 -4
  68. package/src/storage/Storage.ts +50 -35
  69. package/src/storage/knex/KnexStorage.ts +101 -76
package/src/Engine.ts CHANGED
@@ -1,419 +1,494 @@
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"
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 "./TaggedBEEF.js"
6
+ import { TaggedBEEF } from './TaggedBEEF.js'
7
7
  import { STEAK } from './STEAK.js'
8
- import { LookupQuestion } from "./LookupQuestion.js"
9
- import { LookupAnswer } from "./LookupAnswer.js"
10
- import { LookupFormula } from "./LookupFormula.js"
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 default 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 {ProofNotifier[]} [proofNotifiers] - proof notifier services coming soon!
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 proofNotifiers: ProofNotifier[]
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]) throw new Error(`This server does not support this topic: ${t}`)
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
- // Validate the transaction SPV information
43
- const tx = Transaction.fromBEEF(taggedBEEF.beef)
44
- const txid = tx.id('hex') as string
45
- const txValid = await tx.verify(this.chainTracker)
46
- if (!txValid) throw new Error('Unable to verify SPV information.')
47
-
48
- // Find UTXOs belonging to a particular topic
49
- const steak: STEAK = {}
50
- for (const topic of taggedBEEF.topics) {
51
- // Ensure transaction is not already applied to the topic
52
- const dupeCheck = await this.storage.doesAppliedTransactionExist({
53
- txid,
54
- topic
55
- })
56
- if (dupeCheck) {
57
- // The transaction was already processed.
58
- // Currently, NO OUTPUTS ARE ADMITTED FOR DUPLICATE TRANSACTIONS.
59
- // 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.
60
- // This was decided against, because we don't want to encourage unnecessary flooding of duplicative transactions to overlay services.
61
- steak[topic] = {
62
- outputsToAdmit: [],
63
- coinsToRetain: []
64
- }
65
- continue
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
- admissableOutputs = await this.managers[topic].identifyAdmissibleOutputs(taggedBEEF.beef, previousCoins)
107
- } catch (_) {
108
- // If the topic manager throws an error, other topics may still succeed, so we continue to the next one.
109
- // 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.
110
- steak[topic] = {
111
- outputsToAdmit: [],
112
- coinsToRetain: []
113
- }
114
- continue
115
- }
116
-
117
- // Keep track of which outputs to admit, mark as stale, or retain
118
- let outputsToAdmit: number[] = admissableOutputs.outputsToAdmit
119
- let staleCoins: {
120
- txid: string
121
- outputIndex: number
122
- }[] = []
123
- let outputsConsumed: {
124
- txid: string
125
- outputIndex: number
126
- }[] = []
127
-
128
- // Find which outputs should not be retained and mark them as stale
129
- // 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.
130
- for (const inputIndex of previousCoins) {
131
- const previousTXID = tx.inputs[inputIndex].sourceTXID || tx.inputs[inputIndex].sourceTransaction?.id('hex') as string
132
- const previousOutputIndex = tx.inputs[inputIndex].sourceOutputIndex
133
- if (!admissableOutputs.coinsToRetain.includes(inputIndex)) {
134
- staleCoins.push({
135
- txid: previousTXID,
136
- outputIndex: previousOutputIndex
137
- })
138
- } else {
139
- outputsConsumed.push({
140
- txid: previousTXID,
141
- outputIndex: previousOutputIndex
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
- // Remove stale outputs recursively
147
- for (const coin of staleCoins) {
148
- const output = await this.storage.findOutput(coin.txid, coin.outputIndex, topic)
149
- if (output) {
150
- await this.deleteUTXODeep(output)
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
- // Handle admittance and notification of incoming UTXOs
155
- const newUTXOs: { txid: string, outputIndex: number }[] = []
156
- for (const outputIndex of outputsToAdmit) {
157
- // Store the output
158
- await this.storage.insertOutput({
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
- // Update each output consumed to know who consumed it
185
- for (const output of outputsConsumed) {
186
- const outputToUpdate = await this.storage.findOutput(output.txid, output.outputIndex, topic)
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
- // Insert the applied transaction to prevent duplicate processing
195
- await this.storage.insertAppliedTransaction({
196
- txid,
197
- topic
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
- // Keep track of what outputs were admitted for what topic
201
- steak[topic] = admissableOutputs
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
- return steak
205
- // TODO subscribe to get notified by proof notifiers when proof is found for TX if not already present, so the tree can be chopped down
206
- // TODO propagate transaction to other nodes according to synchronization agreements
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
- * Submit a lookup question to the Overlay Services Engine, and receive bakc a Lookup Answer
211
- * @param LookupQuestion — The question to ask the Overlay Services Engine
212
- * @returns The answer to the question
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
- const hydratedOutputs: { beef: number[], outputIndex: number }[] = []
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
- for (const { txid, outputIndex, history } of lookupResult) {
229
- // Make sure this is an unspent output (UTXO)
230
- const UTXO = await this.storage.findOutput(
231
- txid,
232
- outputIndex,
233
- undefined,
234
- false
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
- if (!UTXO) continue
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
- * Traverse and return the history of a UTXO
255
- * @param historySelector
256
- * @param currentDepth
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
- // Determine if history traversal should continue for the current node
273
- let shouldTraverseHistory
274
- if (typeof historySelector !== 'number') {
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
- if (shouldTraverseHistory === false) {
281
- return undefined
282
- } else if (output && output.outputsConsumed.length === 0) {
283
- return output
374
+ // Make sure an output was found
375
+ if (staleOutput === null || staleOutput === undefined) {
376
+ return undefined
284
377
  }
285
378
 
286
- try {
287
- // Query the storage engine for UTXOs consumed by this UTXO
288
- // Only retrieve unique values in case outputs are doubly referenced
289
- const outputsConsumed: { txid: string, outputIndex: number }[] = output.outputsConsumed
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
- // If there are no more consumed utxos, return
356
- if (output.outputsConsumed.length === 0) {
357
- return
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
- * Find a list of supported topic managers
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
- * Run a query to get the documentation for a particular topic manager
404
- * @public
405
- * @returns {Promise<string>} - the documentation for the topic manager
406
- */
407
- async getDocumentationForTopicManger(manager: any): Promise<string> {
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
- * Run a query to get the documentation for a particular lookup service
413
- * @public
414
- * @returns {Promise<string>} - the documentation for the lookup service
415
- */
416
- async getDocumentationForLookupServiceProvider(provider: any): Promise<string> {
417
- return this.lookupServices[provider].getDocumentation?.() || 'No documentation found!'
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
  }