@bsv/overlay 2.2.0 → 2.2.7

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 (155) hide show
  1. package/LICENSE.txt +57 -27
  2. package/README.md +102 -266
  3. package/dist/cjs/mod.d.ts +15 -0
  4. package/dist/cjs/mod.d.ts.map +1 -0
  5. package/dist/cjs/mod.js +4 -1
  6. package/dist/cjs/mod.js.map +1 -1
  7. package/dist/cjs/package.json +46 -18
  8. package/dist/cjs/src/Advertisement.d.ts +9 -0
  9. package/dist/cjs/src/Advertisement.d.ts.map +1 -0
  10. package/dist/cjs/src/Advertiser.d.ts +36 -0
  11. package/dist/cjs/src/Advertiser.d.ts.map +1 -0
  12. package/dist/cjs/src/BASM.d.ts +110 -0
  13. package/dist/cjs/src/BASM.d.ts.map +1 -0
  14. package/dist/cjs/src/BASM.js.map +1 -1
  15. package/dist/cjs/src/BASMRemote.d.ts +14 -0
  16. package/dist/cjs/src/BASMRemote.d.ts.map +1 -0
  17. package/dist/cjs/src/BASMRemote.js.map +1 -1
  18. package/dist/cjs/src/Engine.d.ts +377 -0
  19. package/dist/cjs/src/Engine.d.ts.map +1 -0
  20. package/dist/cjs/src/Engine.js +430 -436
  21. package/dist/cjs/src/Engine.js.map +1 -1
  22. package/dist/cjs/src/GASP/OverlayGASPRemote.d.ts +24 -0
  23. package/dist/cjs/src/GASP/OverlayGASPRemote.d.ts.map +1 -0
  24. package/dist/cjs/src/GASP/OverlayGASPRemote.js +7 -10
  25. package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -1
  26. package/dist/cjs/src/GASP/OverlayGASPStorage.d.ts +103 -0
  27. package/dist/cjs/src/GASP/OverlayGASPStorage.d.ts.map +1 -0
  28. package/dist/cjs/src/GASP/OverlayGASPStorage.js +34 -31
  29. package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -1
  30. package/dist/cjs/src/LookupFormula.d.ts +29 -0
  31. package/dist/cjs/src/LookupFormula.d.ts.map +1 -0
  32. package/dist/cjs/src/LookupService.d.ts +85 -0
  33. package/dist/cjs/src/LookupService.d.ts.map +1 -0
  34. package/dist/cjs/src/Output.d.ts +32 -0
  35. package/dist/cjs/src/Output.d.ts.map +1 -0
  36. package/dist/cjs/src/SafeLog.d.ts +14 -0
  37. package/dist/cjs/src/SafeLog.d.ts.map +1 -0
  38. package/dist/cjs/src/SafeLog.js +44 -0
  39. package/dist/cjs/src/SafeLog.js.map +1 -0
  40. package/{src/SyncConfiguration.ts → dist/cjs/src/SyncConfiguration.d.ts} +2 -1
  41. package/dist/cjs/src/SyncConfiguration.d.ts.map +1 -0
  42. package/dist/cjs/src/TopicManager.d.ts +35 -0
  43. package/dist/cjs/src/TopicManager.d.ts.map +1 -0
  44. package/dist/cjs/src/storage/Storage.d.ts +237 -0
  45. package/dist/cjs/src/storage/Storage.d.ts.map +1 -0
  46. package/dist/cjs/src/storage/knex/KnexStorage.d.ts +69 -0
  47. package/dist/cjs/src/storage/knex/KnexStorage.d.ts.map +1 -0
  48. package/dist/cjs/src/storage/knex/KnexStorage.js +7 -10
  49. package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
  50. package/dist/cjs/src/storage/knex/all-migrations.d.ts +11 -0
  51. package/dist/cjs/src/storage/knex/all-migrations.d.ts.map +1 -0
  52. package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -1
  53. package/dist/cjs/src/storage/knex/migrations/2024-05-18-001-initial.d.ts +4 -0
  54. package/dist/cjs/src/storage/knex/migrations/2024-05-18-001-initial.d.ts.map +1 -0
  55. package/dist/cjs/src/storage/knex/migrations/2024-05-18-001-initial.js.map +1 -1
  56. package/dist/cjs/src/storage/knex/migrations/2024-07-10-001-block-height.d.ts +4 -0
  57. package/dist/cjs/src/storage/knex/migrations/2024-07-10-001-block-height.d.ts.map +1 -0
  58. package/dist/cjs/src/storage/knex/migrations/2024-07-10-001-block-height.js.map +1 -1
  59. package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.d.ts +4 -0
  60. package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.d.ts.map +1 -0
  61. package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.js +36 -12
  62. package/dist/cjs/src/storage/knex/migrations/2024-07-17-001-transactions.js.map +1 -1
  63. package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.d.ts +4 -0
  64. package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.d.ts.map +1 -0
  65. package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.js.map +1 -1
  66. package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.d.ts +4 -0
  67. package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.d.ts.map +1 -0
  68. package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
  69. package/dist/cjs/src/storage/knex/migrations/2025-06-25-001-gasp-pagination-support.d.ts +4 -0
  70. package/dist/cjs/src/storage/knex/migrations/2025-06-25-001-gasp-pagination-support.d.ts.map +1 -0
  71. package/dist/cjs/src/storage/knex/migrations/2025-06-25-001-gasp-pagination-support.js.map +1 -1
  72. package/dist/cjs/src/storage/knex/migrations/2025-07-22-001-fix-score-column-type.d.ts +4 -0
  73. package/dist/cjs/src/storage/knex/migrations/2025-07-22-001-fix-score-column-type.d.ts.map +1 -0
  74. package/dist/cjs/src/storage/knex/migrations/2025-07-22-001-fix-score-column-type.js.map +1 -1
  75. package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.d.ts +9 -0
  76. package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.d.ts.map +1 -0
  77. package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.js.map +1 -1
  78. package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts +4 -0
  79. package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts.map +1 -0
  80. package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -1
  81. package/dist/esm/mod.js +2 -1
  82. package/dist/esm/mod.js.map +1 -1
  83. package/dist/esm/src/BASMRemote.js.map +1 -1
  84. package/dist/esm/src/Engine.js +408 -414
  85. package/dist/esm/src/Engine.js.map +1 -1
  86. package/dist/esm/src/GASP/OverlayGASPRemote.js +2 -2
  87. package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
  88. package/dist/esm/src/GASP/OverlayGASPStorage.js +23 -19
  89. package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
  90. package/dist/esm/src/SafeLog.js +40 -0
  91. package/dist/esm/src/SafeLog.js.map +1 -0
  92. package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
  93. package/dist/esm/src/storage/knex/migrations/2024-07-17-001-transactions.js +36 -12
  94. package/dist/esm/src/storage/knex/migrations/2024-07-17-001-transactions.js.map +1 -1
  95. package/dist/types/mod.d.ts +5 -4
  96. package/dist/types/mod.d.ts.map +1 -1
  97. package/dist/types/src/BASMRemote.d.ts.map +1 -1
  98. package/dist/types/src/Engine.d.ts +15 -0
  99. package/dist/types/src/Engine.d.ts.map +1 -1
  100. package/dist/types/src/GASP/OverlayGASPRemote.d.ts +2 -2
  101. package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
  102. package/dist/types/src/GASP/OverlayGASPStorage.d.ts +3 -1
  103. package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
  104. package/dist/types/src/SafeLog.d.ts +14 -0
  105. package/dist/types/src/SafeLog.d.ts.map +1 -0
  106. package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
  107. package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
  108. package/dist/types/src/storage/knex/migrations/2024-07-17-001-transactions.d.ts.map +1 -1
  109. package/package.json +87 -30
  110. package/dist/cjs/tsconfig.cjs.tsbuildinfo +0 -1
  111. package/dist/esm/tsconfig.esm.tsbuildinfo +0 -1
  112. package/dist/types/tsconfig.types.tsbuildinfo +0 -1
  113. package/docs/API.md +0 -944
  114. package/docs/BRC-136-BASM.md +0 -327
  115. package/docs/README.md +0 -16
  116. package/docs/Synchronization.md +0 -212
  117. package/docs/concepts/00-overview.md +0 -85
  118. package/docs/concepts/01-best-practices.md +0 -202
  119. package/docs/concepts/02-query-performance.md +0 -345
  120. package/docs/concepts/03-database-monitoring.md +0 -211
  121. package/docs/concepts/04-pagination-example.md +0 -186
  122. package/docs/concepts/05-recommendations-summary.md +0 -158
  123. package/docs/concepts/README.md +0 -20
  124. package/docs/examples/README.md +0 -15
  125. package/docs/examples/gs-wip.md +0 -111
  126. package/docs/internal/README.md +0 -11
  127. package/mod.ts +0 -40
  128. package/src/Advertisement.ts +0 -8
  129. package/src/Advertiser.ts +0 -41
  130. package/src/BASM.ts +0 -208
  131. package/src/BASMRemote.ts +0 -54
  132. package/src/Engine.ts +0 -2199
  133. package/src/GASP/OverlayGASPRemote.ts +0 -108
  134. package/src/GASP/OverlayGASPStorage.ts +0 -383
  135. package/src/LookupFormula.ts +0 -31
  136. package/src/LookupService.ts +0 -136
  137. package/src/Output.ts +0 -31
  138. package/src/TopicManager.ts +0 -40
  139. package/src/__tests/BASM.test.ts +0 -44
  140. package/src/__tests/BASMChain.test.ts +0 -290
  141. package/src/__tests/Engine.test.ts +0 -1345
  142. package/src/__tests/OverlayGASPRemote.test.ts +0 -135
  143. package/src/__tests/OverlayGASPStorage.test.ts +0 -171
  144. package/src/storage/Storage.ts +0 -260
  145. package/src/storage/knex/KnexStorage.ts +0 -651
  146. package/src/storage/knex/all-migrations.ts +0 -32
  147. package/src/storage/knex/migrations/2024-05-18-001-initial.ts +0 -30
  148. package/src/storage/knex/migrations/2024-07-10-001-block-height.ts +0 -15
  149. package/src/storage/knex/migrations/2024-07-17-001-transactions.ts +0 -40
  150. package/src/storage/knex/migrations/2024-07-18-001-indexes.ts +0 -30
  151. package/src/storage/knex/migrations/2025-05-28-001-enlarge.ts +0 -42
  152. package/src/storage/knex/migrations/2025-06-25-001-gasp-pagination-support.ts +0 -46
  153. package/src/storage/knex/migrations/2025-07-22-001-fix-score-column-type.ts +0 -24
  154. package/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.ts +0 -18
  155. package/src/storage/knex/migrations/2026-05-29-001-brc136-basm.ts +0 -123
@@ -1,108 +0,0 @@
1
- import { GASPInitialReply, GASPInitialRequest, GASPInitialResponse, GASPNode, GASPNodeResponse, GASPRemote } from '@bsv/gasp'
2
-
3
- export class OverlayGASPRemote implements GASPRemote {
4
- constructor (public endpointURL: string, public topic: string) { }
5
-
6
- /**
7
- * Given an outgoing initial request, sends the request to the foreign instance and obtains their initial response.
8
- * @param request
9
- * @returns
10
- */
11
- async getInitialResponse (request: GASPInitialRequest): Promise<GASPInitialResponse> {
12
- // Send out an HTTP request to the URL (current host for topic)
13
- // Include the topic in the request
14
- // Parse out response and return correct format
15
- const url = `${this.endpointURL}/requestSyncResponse`
16
- const response = await fetch(url, {
17
- method: 'POST',
18
- headers: {
19
- 'Content-Type': 'application/json',
20
- 'X-BSV-Topic': this.topic
21
- },
22
- body: JSON.stringify(request)
23
- })
24
-
25
- if (!response.ok) {
26
- throw new Error(`HTTP error! Status: ${response.status}`)
27
- }
28
-
29
- const result: GASPInitialResponse = await response.json()
30
-
31
- // Validate and return the response in the correct format
32
- if (!Array.isArray(result.UTXOList) || typeof result.since !== 'number') {
33
- throw new TypeError('Invalid response format')
34
- }
35
-
36
- return {
37
- UTXOList: result.UTXOList.map((utxo: any) => ({
38
- txid: utxo.txid,
39
- outputIndex: utxo.outputIndex,
40
- score: utxo.score ?? 0
41
- })),
42
- since: result.since
43
- }
44
- }
45
-
46
- /**
47
- * Given an outgoing txid, outputIndex and optional metadata, request the associated GASP node from the foreign instance.
48
- * @param graphID
49
- * @param txid
50
- * @param outputIndex
51
- * @param metadata
52
- * @returns
53
- */
54
- async requestNode (graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
55
- // Send an HTTP request with the provided info and get back a gaspNode
56
- const url = `${this.endpointURL}/requestForeignGASPNode`
57
- const body = {
58
- graphID,
59
- txid,
60
- outputIndex,
61
- metadata
62
- }
63
-
64
- const response = await fetch(url, {
65
- method: 'POST',
66
- headers: {
67
- 'Content-Type': 'application/json'
68
- },
69
- body: JSON.stringify(body)
70
- })
71
-
72
- if (!response.ok) {
73
- throw new Error(`HTTP error! Status: ${response.status}`)
74
- }
75
-
76
- const result = await response.json()
77
-
78
- // Validate and return the response in the correct format
79
- if (typeof result.graphID !== 'string' || typeof result.rawTx !== 'string' || typeof result.outputIndex !== 'number') {
80
- throw new TypeError('Invalid response format')
81
- }
82
-
83
- const gaspNode: GASPNode = {
84
- graphID: result.graphID,
85
- rawTx: result.rawTx,
86
- outputIndex: result.outputIndex,
87
- proof: result.proof,
88
- txMetadata: result.txMetadata,
89
- outputMetadata: result.outputMetadata,
90
- inputs: result.inputs
91
- }
92
-
93
- return gaspNode
94
- }
95
-
96
- // ---- Now optional methods ----
97
-
98
- // When are only syncing to them
99
- async getInitialReply (response: GASPInitialResponse): Promise<GASPInitialReply> {
100
- throw new Error('Function not supported!')
101
- }
102
-
103
- // Only used when supporting bidirectional sync.
104
- // Overlay services does not support this.
105
- async submitNode (node: GASPNode): Promise<GASPNodeResponse | undefined> {
106
- throw new Error('Node submission not supported!')
107
- }
108
- }
@@ -1,383 +0,0 @@
1
- import { GASPNode, GASPNodeResponse, GASPStorage, GASPOutput } from '@bsv/gasp'
2
- import { MerklePath, Transaction, Utils } from '@bsv/sdk'
3
- import { Engine } from '../Engine.js'
4
-
5
- /**
6
- * Represents a node in the temporary graph.
7
- */
8
- export interface GraphNode {
9
- txid: string
10
- graphID: string
11
- rawTx: string
12
- outputIndex: number
13
- spentBy?: string
14
- proof?: string
15
- txMetadata?: string
16
- outputMetadata?: string
17
- inputs?: Record<string, { hash: string }>
18
- children: GraphNode[]
19
- parent?: GraphNode
20
- }
21
-
22
- export class OverlayGASPStorage implements GASPStorage {
23
- readonly temporaryGraphNodeRefs: Record<string, GraphNode> = {}
24
- private static activeAnchorValidations = 0
25
- private static readonly anchorValidationQueue: Array<() => void> = []
26
- private static activeFinalizations = 0
27
- private static readonly finalizationQueue: Array<() => void> = []
28
- private static readonly MAX_CONCURRENT_ANCHOR_VALIDATIONS = 4
29
- private static readonly MAX_CONCURRENT_FINALIZATIONS = 2
30
-
31
- constructor (public topic: string, public engine: Engine, public maxNodesInGraph?: number) { }
32
-
33
- private static async acquireAnchorValidationSlot (): Promise<void> {
34
- if (OverlayGASPStorage.activeAnchorValidations >= OverlayGASPStorage.MAX_CONCURRENT_ANCHOR_VALIDATIONS) {
35
- await new Promise<void>(resolve => { OverlayGASPStorage.anchorValidationQueue.push(resolve) })
36
- }
37
- OverlayGASPStorage.activeAnchorValidations++
38
- }
39
-
40
- private static releaseAnchorValidationSlot (): void {
41
- OverlayGASPStorage.activeAnchorValidations--
42
- const next = OverlayGASPStorage.anchorValidationQueue.shift()
43
- if (next !== undefined) {
44
- next()
45
- }
46
- }
47
-
48
- private static async acquireFinalizationSlot (): Promise<void> {
49
- if (OverlayGASPStorage.activeFinalizations >= OverlayGASPStorage.MAX_CONCURRENT_FINALIZATIONS) {
50
- await new Promise<void>(resolve => { OverlayGASPStorage.finalizationQueue.push(resolve) })
51
- }
52
- OverlayGASPStorage.activeFinalizations++
53
- }
54
-
55
- private static releaseFinalizationSlot (): void {
56
- OverlayGASPStorage.activeFinalizations--
57
- const next = OverlayGASPStorage.finalizationQueue.shift()
58
- if (next !== undefined) {
59
- next()
60
- }
61
- }
62
-
63
- /**
64
- *
65
- * @param since
66
- * @returns
67
- */
68
- async findKnownUTXOs (since: number): Promise<GASPOutput[]> {
69
- const UTXOs = await this.engine.storage.findUTXOsForTopic(this.topic, since)
70
- return UTXOs.map(output => ({
71
- txid: output.txid,
72
- outputIndex: output.outputIndex,
73
- score: output.score ?? 0
74
- }))
75
- }
76
-
77
- /**
78
- * For a given txid and output index, returns the associated transaction, a merkle proof if the transaction is in a block, and metadata if if requested. If no metadata is requested, metadata hashes on inputs are not returned.
79
- * @param graphID
80
- * @param txid
81
- * @param outputIndex
82
- * @param metadata
83
- * @returns
84
- */
85
- async hydrateGASPNode (graphID: string, txid: string, outputIndex: number, metadata: boolean): Promise<GASPNode> {
86
- const output = await this.engine.storage.findOutput(txid, outputIndex, undefined, undefined, true)
87
-
88
- if (output?.beef === undefined) {
89
- throw new Error('No matching output found!')
90
- }
91
-
92
- const tx = Transaction.fromBEEF(output.beef)
93
- const rawTx = tx.toHex()
94
-
95
- const node: GASPNode = {
96
- rawTx,
97
- graphID,
98
- outputIndex
99
- }
100
- if (tx.merklePath !== undefined) {
101
- node.proof = tx.merklePath.toHex()
102
- }
103
-
104
- return node
105
- }
106
-
107
- /**
108
- * For a given node, returns the inputs needed to complete the graph, including whether updated metadata is requested for those inputs.
109
- * @param tx The node for which needed inputs should be found.
110
- * @returns A promise for a mapping of requested input transactions and whether metadata should be provided for each.
111
- */
112
- async findNeededInputs (tx: GASPNode): Promise<GASPNodeResponse | undefined> {
113
- // If there is no Merkle proof, we always need the inputs
114
- const response: GASPNodeResponse = {
115
- requestedInputs: {}
116
- }
117
- const parsedTx = Transaction.fromHex(tx.rawTx)
118
- if (tx.proof === undefined) {
119
- for (const input of parsedTx.inputs) {
120
- response.requestedInputs[`${input.sourceTXID ?? ''}.${input.sourceOutputIndex}`] = {
121
- metadata: false
122
- }
123
- }
124
-
125
- return await this.stripAlreadyKnownInputs(response)
126
- }
127
-
128
- // Attempt to check if the current transaction is admissible
129
- parsedTx.merklePath = MerklePath.fromHex(tx.proof)
130
- const admittanceResult = await this.engine.managers[this.topic].identifyAdmissibleOutputs(
131
- parsedTx.toBEEF(),
132
- [],
133
- typeof tx.txMetadata === 'string' ? Utils.toArray(tx.txMetadata) : undefined,
134
- 'historical-tx'
135
- )
136
- if (!admittanceResult.outputsToAdmit.includes(tx.outputIndex) && this.engine.managers[this.topic] !== undefined && typeof this.engine.managers[this.topic].identifyNeededInputs === 'function') {
137
- // The transaction is not admissible, get inputs needed for further verification
138
- // TopicManagers should implement a function to identify which inputs are needed.
139
- try {
140
- const neededInputs = await this.engine.managers[this.topic].identifyNeededInputs?.(parsedTx.toBEEF()) ?? []
141
- for (const input of neededInputs) {
142
- response.requestedInputs[`${input.txid}.${input.outputIndex}`] = {
143
- metadata: false
144
- }
145
- }
146
- return await this.stripAlreadyKnownInputs(response)
147
- } catch (e) {
148
- console.error(`An error occurred when identifying needed inputs for transaction: ${parsedTx.id('hex')}.${tx.outputIndex}: ${e}`)
149
- // Cut off the graph in case of an error here.
150
- }
151
- // By default, if the topic manager isn't able to stipulate needed inputs, only the inputs necessary for SPV are requested.
152
- }
153
- // Everything else falls through to returning undefined/void, which will terminate the synchronization at this point.
154
- }
155
-
156
- /**
157
- * Ensures that no inputs are requested from foreign nodes before sending any GASP response
158
- * Also terminates graphs if the response would be empty.
159
- */
160
- private async stripAlreadyKnownInputs (response: GASPNodeResponse | undefined): Promise<GASPNodeResponse | undefined> {
161
- if (response === undefined) {
162
- return response
163
- }
164
- for (const inputNodeId of Object.keys(response.requestedInputs)) {
165
- const [txid, outputIndex] = inputNodeId.split('.')
166
- const found = await this.engine.storage.findOutput(txid, Number(outputIndex), this.topic)
167
- if (found !== null && found !== undefined) {
168
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
169
- delete response.requestedInputs[inputNodeId]
170
- }
171
- }
172
- if (Object.keys(response.requestedInputs).length === 0) {
173
- return undefined
174
- }
175
- return response
176
- }
177
-
178
- /**
179
- * Appends a new node to a temporary graph.
180
- * @param tx The node to append to this graph.
181
- * @param spentBy Unless this is the same node identified by the graph ID, denotes the TXID and input index for the node which spent this one, in 36-byte format.
182
- * @throws If the node cannot be appended to the graph, either because the graph ID is for a graph the recipient does not want or because the graph has grown to be too large before being finalized.
183
- */
184
- async appendToGraph (tx: GASPNode, spentBy?: string | undefined): Promise<void> {
185
- if (this.maxNodesInGraph !== undefined && Object.keys(this.temporaryGraphNodeRefs).length >= this.maxNodesInGraph) {
186
- throw new Error('The max number of nodes in transaction graph has been reached!')
187
- }
188
-
189
- const parsedTx = Transaction.fromHex(tx.rawTx)
190
- const txid = parsedTx.id('hex')
191
- if (tx.proof !== undefined) {
192
- parsedTx.merklePath = MerklePath.fromHex(tx.proof)
193
- }
194
-
195
- // Given the passed in node, append to the temp graph
196
- // Use the spentBy param which should be a txid.inputIndex for the node which spent this one in 36-byte format
197
- const newGraphNode: GraphNode = {
198
- txid,
199
- graphID: tx.graphID,
200
- rawTx: tx.rawTx,
201
- outputIndex: tx.outputIndex,
202
- proof: tx.proof,
203
- txMetadata: tx.txMetadata,
204
- outputMetadata: tx.outputMetadata,
205
- inputs: tx.inputs,
206
- children: []
207
- }
208
-
209
- // If spentBy is undefined, then we know it's the root node.
210
- if (spentBy === undefined) {
211
- this.temporaryGraphNodeRefs[tx.graphID] = newGraphNode
212
- } else {
213
- // Find the parent node based on spentBy
214
- const parentNode = this.temporaryGraphNodeRefs[spentBy]
215
-
216
- if (parentNode === undefined) {
217
- throw new Error(`Parent node with GraphID ${spentBy} not found`)
218
- }
219
- // Set parent-child relationship
220
- parentNode.children.push(newGraphNode)
221
- newGraphNode.parent = parentNode
222
- this.temporaryGraphNodeRefs[`${newGraphNode.txid}.${newGraphNode.outputIndex}`] = newGraphNode
223
- }
224
- }
225
-
226
- /**
227
- * Checks whether the given graph, in its current state, makes reference only to transactions that are proven in the blockchain, or already known by the recipient to be valid.
228
- * Additionally, in a breadth-first manner (ensuring that all inputs for any given node are processed before nodes that spend them), it ensures that the root node remains valid according to the rules of the overlay's topic manager,
229
- * while considering any coins which the Manager had previously indicated were either valid or invalid.
230
- * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
231
- * @throws If the graph is not well-anchored, according to the rules of Bitcoin or the rules of the Overlay Topic Manager.
232
- */
233
- async validateGraphAnchor (graphID: string): Promise<void> {
234
- await OverlayGASPStorage.acquireAnchorValidationSlot()
235
- try {
236
- const rootNode = this.temporaryGraphNodeRefs[graphID]
237
- if (rootNode === undefined) {
238
- throw new Error(`Graph node with ID ${graphID} not found`)
239
- }
240
-
241
- // Check that the root node is Bitcoin-valid.
242
- const beef = this.getBEEFForNode(rootNode)
243
- const spvTx = Transaction.fromBEEF(beef)
244
- const isBitcoinValid = await spvTx.verify(this.engine.chainTracker)
245
- if (!isBitcoinValid) {
246
- throw new Error('The graph is not well-anchored according to the rules of Bitcoin.')
247
- }
248
-
249
- // Then, ensure the node is Overlay-valid.
250
- const beefs = this.computeOrderedBEEFsForGraph(graphID)
251
-
252
- // coins: a Set of all historical coins to retain (no need to remove them), used to emulate topical admittance of previous inputs over time.
253
- const coins = new Set<string>()
254
-
255
- // Submit all historical BEEFs in order through the topic manager, tracking what would be retained until we submit the root node last.
256
- // If, at the end, the root node is admitted, we have a valid overlay-specific graph.
257
- for (const beef of beefs) {
258
- // For any input to this transaction, see if it's a valid coin that's admitted. If so, it's a previous coin.
259
- const previousCoins: number[] = []
260
- const tx = Transaction.fromBEEF(beef)
261
- for (const [inputIndex, input] of tx.inputs.entries()) {
262
- const sourceTXID = input.sourceTXID ?? input.sourceTransaction?.id('hex')
263
- if (sourceTXID != null && sourceTXID !== '') {
264
- const coin = `${sourceTXID}.${input.sourceOutputIndex}`
265
- if (coins.has(coin)) {
266
- previousCoins.push(Number(inputIndex))
267
- }
268
- }
269
- }
270
- const admittanceInstructions = await this.engine.managers[this.topic].identifyAdmissibleOutputs(
271
- beef,
272
- previousCoins,
273
- undefined,
274
- 'historical-tx'
275
- )
276
- // Every admitted output is now a coin.
277
- for (const outputIndex of admittanceInstructions.outputsToAdmit) {
278
- coins.add(`${tx.id('hex')}.${outputIndex}`)
279
- }
280
- }
281
- // After sending through all the graph's BEEFs...
282
- // If the root node is now a coin, we have acceptance by the overlay.
283
- // Otherwise, throw.
284
- if (!coins.has(graphID)) {
285
- throw new Error('This graph did not result in topical admittance of the root node. Rejecting.')
286
- }
287
- } finally {
288
- OverlayGASPStorage.releaseAnchorValidationSlot()
289
- }
290
- }
291
-
292
- /**
293
- * Deletes all data associated with a temporary graph that has failed to sync, if the graph exists.
294
- * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the tip of this graph.
295
- */
296
- async discardGraph (graphID: string): Promise<void> {
297
- for (const [nodeId, graphRef] of Object.entries(this.temporaryGraphNodeRefs)) {
298
- if (graphRef.graphID === graphID) {
299
- // Delete child node
300
- // eslint-disable-next-line @typescript-eslint/no-dynamic-delete
301
- delete this.temporaryGraphNodeRefs[nodeId]
302
- }
303
- }
304
- }
305
-
306
- /**
307
- * Finalizes a graph, solidifying the new UTXO and its ancestors so that it will appear in the list of known UTXOs.
308
- * @param graphID The TXID and output index (in 36-byte format) for the UTXO at the root of this graph.
309
- */
310
- async finalizeGraph (graphID: string): Promise<void> {
311
- await OverlayGASPStorage.acquireFinalizationSlot()
312
- try {
313
- const beefs = this.computeOrderedBEEFsForGraph(graphID)
314
-
315
- // Submit all historical BEEFs in order, finalizing the graph for the current UTXO.
316
- // We skip SPV verification here because validateGraphAnchor has already done it.
317
- for (const beef of beefs) {
318
- await this.engine.submit({
319
- beef,
320
- topics: [this.topic]
321
- }, () => { }, 'historical-tx-no-spv')
322
- }
323
- } finally {
324
- OverlayGASPStorage.releaseFinalizationSlot()
325
- }
326
- }
327
-
328
- /**
329
- * Computes an ordered set of BEEFs for the graph with the given graph IDs
330
- * @param {string} graphID — The ID of the graph for which BEEFs are required
331
- * @returns Ordered BEEFs for the graph
332
- */
333
- private computeOrderedBEEFsForGraph (graphID: string): number[][] {
334
- const beefs: number[][] = []
335
- const hydrator = (node: GraphNode): void => {
336
- const currentBEEF = this.getBEEFForNode(node)
337
- if (!beefs.includes(currentBEEF)) {
338
- beefs.unshift(currentBEEF)
339
- }
340
-
341
- for (const child of node.children) {
342
- // Continue backwards to the earliest nodes, adding them onto the beginning
343
- hydrator(child)
344
- }
345
- }
346
-
347
- // Start the hydrator with the root node
348
- const foundRoot = this.temporaryGraphNodeRefs[graphID]
349
- if (foundRoot == null) {
350
- throw new Error('Unable to find root node in graph for finalization!')
351
- }
352
- hydrator(foundRoot)
353
- return beefs
354
- }
355
-
356
- /**
357
- * Computes a full BEEF for a given graph node, based on the temporary graph store.
358
- * @param node Graph node for which BEEF is needed.
359
- * @returns BEEF array, including all proofs on inputs.
360
- */
361
- private getBEEFForNode (node: GraphNode): number[] {
362
- // Given a node, hydrate its merkle proof or all inputs, returning a reference to the hydrated node's Transaction object
363
- const hydrator = (node: GraphNode): Transaction => {
364
- const tx = Transaction.fromHex(node.rawTx)
365
- if (node.proof != null && node.proof !== '') {
366
- tx.merklePath = MerklePath.fromHex(node.proof)
367
- return tx // Transaction with proof, end of the line.
368
- }
369
- // For each input, look it up and recurse.
370
- for (const [inputIndex, input] of tx.inputs.entries()) {
371
- const foundNode = this.temporaryGraphNodeRefs[`${input.sourceTXID ?? ''}.${input.sourceOutputIndex}`]
372
- if (foundNode == null) {
373
- throw new Error('Required input node for unproven parent not found in temporary graph store. Ensure, for every parent of any given already-proven node (kept for Overlay-specific historical reasons), that a proof is also provided on those inputs. While implicitly they are valid by virtue of their descendents being proven in the blockchain, BEEF serialization will still fail when winding forward the topical UTXO set histories during sync.')
374
- }
375
- tx.inputs[inputIndex].sourceTransaction = hydrator(foundNode)
376
- }
377
- return tx
378
- }
379
-
380
- const finalTX = hydrator(node)
381
- return finalTX.toBEEF()
382
- }
383
- }
@@ -1,31 +0,0 @@
1
- /**
2
- * The formula that will be used by the Overlay Services Engine to compute the Lookup Answer. Can be returned by Lookup Services in response to a Lookup Question.
3
- */
4
- export type LookupFormula = Array<{
5
- /**
6
- * TXID of the transaction where an output responsive to the Lookup Question resides.
7
- */
8
- txid: string
9
-
10
- /**
11
- * Index of the transaction output responsive to the Lookup Question.
12
- */
13
- outputIndex: number
14
-
15
- /**
16
- * Decides what history to incorporate into the Lookup Answer.
17
- *
18
- * Optionally directs the Overlay Services Engine as to the historical context (preceding outputs) to include as part of the Lookup Answer.
19
- * - If a number, denotes how many previous spends (in terms of chain depth) the Engine should include with the Answer.
20
- * - If a decider function, accepts a BEEF-formatted transaction, an output index and the current depth (relative to the top-level responsive UTXO) as parameters.
21
- * The function returns a promise for a boolean indicating whether the output should be incorporated into the Lookup Answer.
22
- * If so, the function will be called again for transactions that preceded the one provided, ultimately allowing the complete shape of the responsive spend history to be described.
23
- * If not provided, no historical information will be included with the Lookup Answer, except that which may incidentally be required to fully anchor the Lookup Answer to the blockchain.
24
- */
25
- history?: ((beef: number[], outputIndex: number, currentDepth: number) => Promise<boolean>) | number
26
-
27
- /**
28
- * Context for the UTXO, derived from the off-chain values.
29
- */
30
- context?: number[]
31
- }>
@@ -1,136 +0,0 @@
1
- import { LookupFormula } from './LookupFormula.js'
2
- import { Script, LookupQuestion } from '@bsv/sdk'
3
-
4
-
5
- /* ---------------------------------------------------------------------------
6
- * Modes a Lookup Service may request from the Overlay Services Engine
7
- * -------------------------------------------------------------------------- */
8
- export type AdmissionMode = 'locking-script' | 'whole-tx'
9
- export type SpendNotificationMode = 'none' | 'txid' | 'script' | 'whole-tx'
10
-
11
- /* ---------------------------------------------------------------------------
12
- * Admission-Notification payloads
13
- * -------------------------------------------------------------------------- */
14
- export type OutputAdmittedByTopic =
15
- | { // «locking-script» mode
16
- mode: 'locking-script'
17
- txid: string // ID of the *admitted* transaction
18
- outputIndex: number // index of admitted output
19
- topic: string // topic into which it was admitted
20
- satoshis: number // value of the output
21
- lockingScript: Script // script in this output
22
- offChainValues?: number[] // off-chain values associated with the output
23
- }
24
- | { // «whole-tx» mode
25
- mode: 'whole-tx'
26
- atomicBEEF: number[] // whole transaction (Atomic BEEF)
27
- outputIndex: number
28
- topic: string
29
- offChainValues?: number[]
30
- }
31
-
32
- /* ---------------------------------------------------------------------------
33
- * Spend-Notification payloads
34
- * -------------------------------------------------------------------------- */
35
- export type OutputSpent =
36
- | { // «none» – “it was spent”
37
- mode: 'none'
38
- txid: string
39
- outputIndex: number
40
- topic: string
41
- }
42
- | { // «txid» – pointer only
43
- mode: 'txid'
44
- txid: string
45
- outputIndex: number
46
- topic: string
47
- spendingTxid: string
48
- }
49
- | { // «script» – granular input data
50
- mode: 'script'
51
- txid: string
52
- outputIndex: number
53
- topic: string
54
- spendingTxid: string
55
- inputIndex: number
56
- unlockingScript: Script
57
- sequenceNumber: number
58
- offChainValues?: number[]
59
- }
60
- | { // «whole-tx» – full spending TX
61
- mode: 'whole-tx'
62
- txid: string
63
- outputIndex: number
64
- topic: string
65
- spendingAtomicBEEF: number[]
66
- offChainValues?: number[]
67
- }
68
-
69
- /* ---------------------------------------------------------------------------
70
- * Metadata structure returned by getMetaData()
71
- * -------------------------------------------------------------------------- */
72
- export interface LookupServiceMetaData {
73
- name: string
74
- shortDescription: string
75
- iconURL?: string
76
- version?: string
77
- informationURL?: string
78
- }
79
-
80
- /* ---------------------------------------------------------------------------
81
- * Lookup Service Interface
82
- * -------------------------------------------------------------------------- */
83
- export interface LookupService {
84
- /* -------------------------------------------------------------------------
85
- * REQUIRED static declarations
86
- * ----------------------------------------------------------------------- */
87
- readonly admissionMode: AdmissionMode
88
- readonly spendNotificationMode: SpendNotificationMode
89
-
90
- /* -------------------------------------------------------------------------
91
- * REQUIRED lifecycle hooks
92
- * ----------------------------------------------------------------------- */
93
- /**
94
- * Invoked when a Topic Manager admits a new UTXO.
95
- * The payload shape depends on this.admissionMode.
96
- */
97
- outputAdmittedByTopic: (payload: OutputAdmittedByTopic) => Promise<void> | void
98
-
99
- /**
100
- * Invoked when a previously-admitted UTXO is spent.
101
- * The payload shape depends on this.spendNotificationMode.
102
- */
103
- outputSpent?: (payload: OutputSpent) => Promise<void> | void
104
-
105
- /**
106
- * Called when a Topic Manager decides that **historical retention** of the
107
- * specified UTXO is no longer required.
108
- */
109
- outputNoLongerRetainedInHistory?: (
110
- txid: string,
111
- outputIndex: number,
112
- topic: string
113
- ) => Promise<void> | void
114
-
115
- /**
116
- * LEGAL EVICTION:
117
- * Permanently remove the referenced UTXO from all indices maintained by the
118
- * Lookup Service. After eviction the service MUST NOT reference the output
119
- * in any future lookup answer.
120
- */
121
- outputEvicted: (
122
- txid: string,
123
- outputIndex: number
124
- ) => Promise<void> | void
125
-
126
- /* -------------------------------------------------------------------------
127
- * Query API
128
- * ----------------------------------------------------------------------- */
129
- lookup: (question: LookupQuestion) => Promise<LookupFormula>
130
-
131
- /* -------------------------------------------------------------------------
132
- * Documentation helpers
133
- * ----------------------------------------------------------------------- */
134
- getDocumentation: () => Promise<string>
135
- getMetaData: () => Promise<LookupServiceMetaData>
136
- }
package/src/Output.ts DELETED
@@ -1,31 +0,0 @@
1
- /**
2
- * Represents an output to be tracked by the Overlay Services Engine
3
- */
4
- export interface Output {
5
- /** TXID of the output */
6
- txid: string
7
- /** index of the output */
8
- outputIndex: number
9
- /** script of the output */
10
- outputScript: number[]
11
- /** number of satoshis in the output */
12
- satoshis: number
13
- /** topic to which the output belongs */
14
- topic: string
15
- /** Whether the output is spent */
16
- spent: boolean
17
- /** Outputs consumed by the transaction associated with the output */
18
- outputsConsumed: Array<{
19
- txid: string
20
- outputIndex: number
21
- }>
22
- /** Outputs consuming this output */
23
- consumedBy: Array<{
24
- txid: string
25
- outputIndex: number
26
- }>
27
- /** The transaction data for the output */
28
- beef?: number[]
29
- blockHeight?: number
30
- score?: number
31
- }