@bsv/overlay 0.1.0-alpha.9 → 0.1.1

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 (50) hide show
  1. package/dist/cjs/package.json +2 -1
  2. package/dist/cjs/src/Engine.js +560 -41
  3. package/dist/cjs/src/Engine.js.map +1 -1
  4. package/dist/cjs/src/GASP/OverlayGASPRemote.js +96 -0
  5. package/dist/cjs/src/GASP/OverlayGASPRemote.js.map +1 -0
  6. package/dist/cjs/src/GASP/OverlayGASPStorage.js +221 -0
  7. package/dist/cjs/src/GASP/OverlayGASPStorage.js.map +1 -0
  8. package/dist/cjs/src/SyncConfiguration.js +3 -0
  9. package/dist/cjs/src/SyncConfiguration.js.map +1 -0
  10. package/dist/cjs/src/storage/knex/KnexStorage.js +20 -0
  11. package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -1
  12. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  13. package/dist/esm/src/Engine.js +562 -41
  14. package/dist/esm/src/Engine.js.map +1 -1
  15. package/dist/esm/src/GASP/OverlayGASPRemote.js +93 -0
  16. package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -0
  17. package/dist/esm/src/GASP/OverlayGASPStorage.js +219 -0
  18. package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -0
  19. package/dist/esm/src/SyncConfiguration.js +2 -0
  20. package/dist/esm/src/SyncConfiguration.js.map +1 -0
  21. package/dist/esm/src/storage/knex/KnexStorage.js +20 -0
  22. package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
  23. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  24. package/dist/types/mod.d.ts +1 -1
  25. package/dist/types/mod.d.ts.map +1 -1
  26. package/dist/types/src/Engine.d.ts +161 -9
  27. package/dist/types/src/Engine.d.ts.map +1 -1
  28. package/dist/types/src/GASP/OverlayGASPRemote.d.ts +23 -0
  29. package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -0
  30. package/dist/types/src/GASP/OverlayGASPStorage.d.ts +68 -0
  31. package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -0
  32. package/dist/types/src/SyncConfiguration.d.ts +20 -0
  33. package/dist/types/src/SyncConfiguration.d.ts.map +1 -0
  34. package/dist/types/src/TopicManager.d.ts +8 -0
  35. package/dist/types/src/TopicManager.d.ts.map +1 -1
  36. package/dist/types/src/storage/Storage.d.ts +7 -0
  37. package/dist/types/src/storage/Storage.d.ts.map +1 -1
  38. package/dist/types/src/storage/knex/KnexStorage.d.ts +1 -0
  39. package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
  40. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  41. package/mod.ts +2 -2
  42. package/package.json +2 -1
  43. package/src/Engine.ts +636 -46
  44. package/src/SyncConfiguration.ts +19 -0
  45. package/src/TopicManager.ts +6 -0
  46. package/src/__tests/Engine.test.ts +151 -25
  47. package/src/__tests/OverlayGASPRemote.test.ts +132 -0
  48. package/src/__tests/OverlayGASPStorage.test.ts +166 -0
  49. package/src/storage/Storage.ts +8 -0
  50. package/src/storage/knex/KnexStorage.ts +26 -0
@@ -0,0 +1,19 @@
1
+ /**
2
+ * Configuration for synchronizing supported topic managers.
3
+ *
4
+ * This configuration determines which topics should support synchronization and specifies the mode of synchronization.
5
+ *
6
+ * There are two synchronization modes:
7
+ * 1. Sync to predefined hardcoded peers for the specified topic, including associated hosting URLs.
8
+ * 2. Use SHIP (Service Host Interconnect Protocol) to sync with all known peers that support the specified topic.
9
+ *
10
+ * Each entry in the configuration object maps a topic to either an array of overlay service peers (hardcoded URLs) or the string 'SHIP' (for dynamic syncing using SHIP).
11
+ *
12
+ * @example
13
+ * // Example usage of SyncConfiguration
14
+ * const config: SyncConfiguration = {
15
+ * "topicManager1": ["http://peer1.com", "http://peer2.com"],
16
+ * "topicManager2": "SHIP"
17
+ * }
18
+ */
19
+ export type SyncConfiguration = Record<string, string[] | 'SHIP'>
@@ -11,6 +11,12 @@ export interface TopicManager {
11
11
  */
12
12
  identifyAdmissibleOutputs: (beef: number[], previousCoins: number[]) => Promise<AdmittanceInstructions>
13
13
 
14
+ /**
15
+ * Identifies and returns the inputs needed to anchor any topical outputs from this transaction to their associated previous history.
16
+ * @throws - if there are no potentially valid topical outputs in this transaction
17
+ */
18
+ identifyNeededInputs?: (beef: number[]) => Promise<Array<{ txid: string, outputIndex: number }>>
19
+
14
20
  /**
15
21
  * Returns a Markdown-formatted documentation string for the topic manager.
16
22
  */
@@ -3,7 +3,7 @@ import { LookupService } from '../LookupService'
3
3
  import { TopicManager } from '../TopicManager'
4
4
  import { AdmittanceInstructions } from '../AdmittanceInstructions'
5
5
  import { Storage } from '../storage/Storage'
6
- import { Transaction } from '@bsv/sdk'
6
+ import { Transaction, Utils } from '@bsv/sdk'
7
7
  import { Output } from '../Output'
8
8
 
9
9
  const mockChainTracker = {
@@ -14,7 +14,7 @@ const BRC62Hex = '0100beef01fe636d0c0007021400fe507c0c7aa754cef1f7889d5fd395cf1f
14
14
  const exampleTX = Transaction.fromHexBEEF(BRC62Hex)
15
15
 
16
16
  const exampleBeef = exampleTX.toBEEF()
17
- const exampleTXID = exampleTX.id('hex') as string
17
+ const exampleTXID = exampleTX.id('hex')
18
18
  const examplePreviousTXID = '3ecead27a44d013ad1aae40038acbb1883ac9242406808bb4667c15b4f164eac'
19
19
  let mockTopicManager: TopicManager, mockLookupService: LookupService, mockStorageEngine: Storage
20
20
  const mockOutput: Output = {
@@ -22,7 +22,7 @@ const mockOutput: Output = {
22
22
  outputIndex: 0,
23
23
  outputScript: exampleTX.outputs[0].lockingScript.toBinary(),
24
24
  topic: 'hello',
25
- satoshis: exampleTX.outputs[0].satoshis as number,
25
+ satoshis: exampleTX.outputs[0].satoshis,
26
26
  beef: exampleBeef,
27
27
  spent: false,
28
28
  outputsConsumed: [],
@@ -60,32 +60,33 @@ describe('BSV Overlay Services Engine', () => {
60
60
  markUTXOAsSpent: jest.fn(),
61
61
  updateConsumedBy: jest.fn(),
62
62
  updateOutputBeef: jest.fn(),
63
- deleteOutput: jest.fn()
63
+ deleteOutput: jest.fn(),
64
+ findUTXOsForTopic: jest.fn()
64
65
  }
65
66
  })
66
67
  describe('handleNewMerkleProof tests', () => {
67
- const mockOutput: Output = {
68
- txid: exampleTXID,
69
- outputIndex: 0,
70
- outputScript: exampleTX.outputs[0].lockingScript.toBinary(),
71
- topic: 'hello',
72
- satoshis: exampleTX.outputs[0].satoshis as number,
73
- beef: exampleBeef,
74
- spent: false,
75
- outputsConsumed: [],
76
- consumedBy: []
77
- }
68
+ const mockOutput: Output = {
69
+ txid: exampleTXID,
70
+ outputIndex: 0,
71
+ outputScript: exampleTX.outputs[0].lockingScript.toBinary(),
72
+ topic: 'hello',
73
+ satoshis: exampleTX.outputs[0].satoshis,
74
+ beef: exampleBeef,
75
+ spent: false,
76
+ outputsConsumed: [],
77
+ consumedBy: []
78
+ }
78
79
  it('0 simple proof', async () => {
79
80
  const beef = beef27c8f_1
80
81
  const txid = txid27c8f
81
82
  const tx = Transaction.fromHexBEEF(beef)
82
83
 
83
84
  const output27c8f_0: Output = {
84
- txid: txid,
85
+ txid,
85
86
  outputIndex: 0,
86
87
  outputScript: tx.outputs[0].lockingScript.toBinary(),
87
88
  topic: 'hello',
88
- satoshis: tx.outputs[0].satoshis as number,
89
+ satoshis: tx.outputs[0].satoshis,
89
90
  beef: tx.toBEEF(),
90
91
  spent: false,
91
92
  outputsConsumed: [],
@@ -93,7 +94,7 @@ const mockOutput: Output = {
93
94
  }
94
95
 
95
96
  mockLookupService.lookup = jest.fn(async () => [{
96
- txid: txid,
97
+ txid,
97
98
  outputIndex: 0,
98
99
  history: 1
99
100
  }])
@@ -115,10 +116,112 @@ const mockOutput: Output = {
115
116
  )
116
117
 
117
118
  const merklePath = Transaction.fromHexBEEF(beef27c8f_0).merklePath
118
- if (!merklePath) throw('improper test setup')
119
+ if (!merklePath) throw ('improper test setup')
119
120
  await engine.handleNewMerkleProof(txid, merklePath)
120
121
  expect(newBEEF.length > 0 && newBEEF.length < beef.length / 2).toBe(true)
121
122
  })
123
+
124
+ it('1 recurse proof', async () => {
125
+ const outputs: Output[] = []
126
+ const findOutput = (txid: string, outputIndex: number): Output => {
127
+ const i = outputs.findIndex(o => o.txid === txid && o.outputIndex === outputIndex)
128
+ if (i < 0) throw new Error(`missing output ${txid} ${outputIndex}`)
129
+ return outputs[i]
130
+ }
131
+
132
+ const addConsumingOutput = (tx: Transaction, outputIndex: number, consumes?: Output): Output => {
133
+ const o: Output = {
134
+ txid: tx.id('hex'),
135
+ outputIndex,
136
+ outputScript: tx.outputs[outputIndex].lockingScript.toBinary(),
137
+ topic: 'hello',
138
+ satoshis: tx.outputs[outputIndex].satoshis,
139
+ beef: tx.toBEEF(),
140
+ spent: false,
141
+ outputsConsumed: [],
142
+ consumedBy: []
143
+ }
144
+ if (consumes) {
145
+ const c = findOutput(consumes.txid, consumes.outputIndex)
146
+ c.consumedBy.push(o)
147
+ o.outputsConsumed.push(c)
148
+ }
149
+ outputs.push(o)
150
+ return o
151
+ }
152
+
153
+ mockLookupService.lookup = jest.fn(async () => [{ txid: txid17d182, outputIndex: 0, history: 1 }])
154
+ const newBEEF: Record<string, string> = {}
155
+ mockStorageEngine.findOutput = jest.fn(async (txid: string, outputIndex: number, topic?: string, spent?: boolean) => {
156
+ return findOutput(txid, outputIndex)
157
+ })
158
+ mockStorageEngine.findOutputsForTransaction = jest.fn(async (txid: string) => {
159
+ const os = outputs.filter(o => o.txid === txid)
160
+ return os
161
+ })
162
+ mockStorageEngine.updateOutputBeef = jest.fn(async (txid: string, outputIndex: number, topic: string, beef: number[]) => {
163
+ newBEEF[`${txid}_${outputIndex}`] = Utils.toHex(beef)
164
+ })
165
+ const engine = new Engine({ Hello: mockTopicManager }, { Hello: mockLookupService }, mockStorageEngine, mockChainTracker)
166
+
167
+ /*
168
+ txid 17d1829ba8424b97369ee8b528ee8b65d9d4b9c08d037d224a7be7c025f78f78 output 0 1196 sats
169
+ txid 9426201003b01e5bb66e9240a1cc337174238e816a25a06ba532fa67af4457d7 output 0 1197 sats
170
+ txid 509f5ef79c18504fdfe21e67608f56bacb8d8a200634e46f60a9dc8430dcd6e9 output 0 1198 sats
171
+ txid 877734db8eb917d0e2174a4bdddc085b34f67cedd6b94628c4feb1b979a2b2c5 output 0 1199 sats
172
+ txid 37abad7168d47d4e107d0f9f96813a4feef6ea346482fce554c3fade85d45409 output 0 1200 sats
173
+ txid d786db393ec7f1315bee2cbd3aa47634394f57c861feed8c075563308f14c237 output 0 1568 sats
174
+ txid a7d4cac391f5b3d8dedaacb3cd8b5afac54df2d8e043ae0452abcfe190686494 output 17 1132 sats
175
+ */
176
+
177
+ const tx17d182 = Transaction.fromHexBEEF(beef17d182_4)
178
+ const tx942620 = tx17d182.inputs[0].sourceTransaction
179
+ const tx509f5e = tx942620.inputs[0].sourceTransaction
180
+ const tx877734 = tx509f5e.inputs[0].sourceTransaction
181
+ const tx37abad = tx877734.inputs[0].sourceTransaction
182
+
183
+ const output37abad_0 = addConsumingOutput(tx37abad, 0)
184
+ const output877734_0 = addConsumingOutput(tx877734, 0, output37abad_0)
185
+ const output509f5e_0 = addConsumingOutput(tx509f5e, 0, output877734_0)
186
+ const output942620_0 = addConsumingOutput(tx942620, 0, output509f5e_0)
187
+ const output17d182_0 = addConsumingOutput(tx17d182, 0, output942620_0)
188
+
189
+ const mp37abad = Transaction.fromHexBEEF(beef37abad_0).merklePath
190
+ await engine.handleNewMerkleProof(txid37abad, mp37abad)
191
+ expect(Object.keys(newBEEF).length).toBe(0)
192
+
193
+ const mp877734 = Transaction.fromHexBEEF(beef877734_0).merklePath
194
+ await engine.handleNewMerkleProof(txid877734, mp877734)
195
+ expect(newBEEF[`${txid877734}_0`]).toBe(beef877734_0)
196
+ expect(newBEEF[`${txid509f5e}_0`].length).toBeGreaterThan(beef509f5e_0.length)
197
+ expect(newBEEF[`${txid942620}_0`].length).toBeGreaterThan(beef942620_0.length)
198
+ expect(newBEEF[`${txid17d182}_0`].length).toBeGreaterThan(beef17d182_0.length)
199
+ expect(Object.keys(newBEEF).length).toBe(4)
200
+
201
+ const mp509f5e = Transaction.fromHexBEEF(beef509f5e_0).merklePath
202
+ await engine.handleNewMerkleProof(txid509f5e, mp509f5e)
203
+ expect(newBEEF[`${txid877734}_0`]).toBe(beef877734_0)
204
+ expect(newBEEF[`${txid509f5e}_0`]).toBe(beef509f5e_0)
205
+ expect(newBEEF[`${txid942620}_0`].length).toBeGreaterThan(beef942620_0.length)
206
+ expect(newBEEF[`${txid17d182}_0`].length).toBeGreaterThan(beef17d182_0.length)
207
+ expect(Object.keys(newBEEF).length).toBe(4)
208
+
209
+ const mp942620 = Transaction.fromHexBEEF(beef942620_0).merklePath
210
+ await engine.handleNewMerkleProof(txid942620, mp942620)
211
+ expect(newBEEF[`${txid877734}_0`]).toBe(beef877734_0)
212
+ expect(newBEEF[`${txid509f5e}_0`]).toBe(beef509f5e_0)
213
+ expect(newBEEF[`${txid942620}_0`]).toBe(beef942620_0)
214
+ expect(newBEEF[`${txid17d182}_0`].length).toBeGreaterThan(beef17d182_0.length)
215
+ expect(Object.keys(newBEEF).length).toBe(4)
216
+
217
+ const mp17d182 = Transaction.fromHexBEEF(beef17d182_0).merklePath
218
+ await engine.handleNewMerkleProof(txid17d182, mp17d182)
219
+ expect(newBEEF[`${txid877734}_0`]).toBe(beef877734_0)
220
+ expect(newBEEF[`${txid509f5e}_0`]).toBe(beef509f5e_0)
221
+ expect(newBEEF[`${txid942620}_0`]).toBe(beef942620_0)
222
+ expect(newBEEF[`${txid17d182}_0`]).toBe(beef17d182_0)
223
+ expect(Object.keys(newBEEF).length).toBe(4)
224
+ })
122
225
  })
123
226
 
124
227
  describe('submit', () => {
@@ -582,7 +685,7 @@ const mockOutput: Output = {
582
685
  query: { name: 'Bob' }
583
686
  })
584
687
  expect(mockStorageEngine.findOutput).toHaveBeenCalledWith(
585
- "mockTXID", 0, undefined, false
688
+ 'mockTXID', 0, undefined, false
586
689
  )
587
690
  })
588
691
  it('Calls getUTXOHistory with the correct UTXO and history parameters', async () => {
@@ -648,7 +751,8 @@ const mockOutput: Output = {
648
751
  outputs: [{
649
752
  beef: mockOutput.beef,
650
753
  outputIndex: mockOutput.outputIndex
651
- }], type: 'output-list'
754
+ }],
755
+ type: 'output-list'
652
756
  })
653
757
  })
654
758
  })
@@ -726,7 +830,7 @@ const mockOutput: Output = {
726
830
  )
727
831
  expect(results).toEqual({
728
832
  outputs: [],
729
- type: "output-list"
833
+ type: 'output-list'
730
834
  })
731
835
  })
732
836
  it('Returns undefined if the history selector is a number, and less than the current depth', async () => {
@@ -754,7 +858,7 @@ const mockOutput: Output = {
754
858
  })
755
859
  expect(results).toEqual({
756
860
  outputs: [],
757
- type: "output-list"
861
+ type: 'output-list'
758
862
  })
759
863
  })
760
864
  it('Returns the current output even if history should be traversed, if the current output is part of a transaction that does not consume any previous topical UTXOs', async () => {
@@ -785,7 +889,8 @@ const mockOutput: Output = {
785
889
  outputs: [{
786
890
  beef: mockOutput.beef,
787
891
  outputIndex: mockOutput.outputIndex
788
- }], type: 'output-list'
892
+ }],
893
+ type: 'output-list'
789
894
  })
790
895
  })
791
896
  // it('Traversing history, calls findOutput with any output consumed by this UTXO', async () => {
@@ -856,4 +961,25 @@ const beef27c8f_1 = '0100beef01feafcf0c000802020044ef36fc6c79af360b13f2164c6e772
856
961
  // beef27c8f_0 is 0 levels deep, i.e. tx has merklePath
857
962
  const beef27c8f_0 = '0100beef01fef4f10c000902fd020100ab2d9b3bbfc2ecf5c834f7719bf10dcabef31cfa3b78fa714bd3a2b3958fc6b5fd0301029515629d935d81e704ca97b8dc02a698c39d78f06bbb3d8d46bcaa5178f3c827018000fc94b1da08b5d0850afcc4948a9e129a2c2c0bb629090ca84fef5d326ceadbc2014100fa78356192a22189dd3c2a03bfb2e043667a55a85386a8d0f0853bbaa54ba748012100e37888006acc37cbfc6a459117c3e957b00e8ff3a90d93bc9aaa3fc9d972453701110052564e444cbe4b1f24015d91f5cfd7eff63e5f403613a7b48e77efa32761f950010900acef65445ef232f1fcb527bab67fccc90ae9d0c610a4f752d84c5e10a3415d74010500e1fadd176551150808ae96ff2b4dff487c432788d23db25fd3fb5e8b3e16b96f0103003e530f146a992ed9378807fa43bc1e6033de0711d728ee3a0ea1507b5ffff2940100006c8eca8a9d680ab2ae4dc1f0e247f282de3f32b99175e6f9ddb4f8e76ae0bf1b010100000001792c8c563b17721744d8f101d79e873aae62816d0fd47dd9d90ada219c1b252d070000006b48304502210090368b5925795abb0415a3cd6265d7abae7d3541fb7f2fdb1fdffdd916b3fce3022068136da44df8ee367807c7c59c950ea8d428adfe142268cd6371cf4c1b4c3208412102858ea1804708d1e16e77b49ba1559b3742797f5f43efd3922fa3d21557521d7cffffffff03f401000000000000fde10121026afd108ddbc05e093207ebdf06557a3d66e15b96accbf1766c2e7d6efa3d1981ac22313852713463583334356b6a7461734467764b41455a435135564c5a72516f76786d2081d846bcae65ce281829b6d81eff3862a68b8eaf4e99ad97c0c4ad2e149631c020f936ace2d2b4034710dec819e343f69ddbb1a16d721b88413b697c2475f75f504c4e8470062e6966cb7ec80864e5e1217ecf2b3f113bb5307e1ec1d338678e54b426a11a267e990f7dcd85072c7b3870fabb54812d7f8a7aaa672ce01e4f8a2c6a1d532849877329bd23be0b3383fcb437ffd1ff05aad824343bfe5bae52d5775ed99ba9d5a72038dd50f81153e674a8b1c5d659aac361e660cc5657b291eda149de2505efb0bbac0a353331393633353238334c797f3040f01934bb25733d4700a0309194a7defd8ceea7a8a59ad279c6237e2917264a115773777c5746546288b15ec1cf6788b801dd3952fec804cadbba2ff4e05464735e2db83c3509b59119efc843716c34becd2884f011f2e7f61f382237891875115b6ea5bac1c3e9ca6c575b44547d589c38c1b5a8cadb463044022016e1fdb226b465f3151aaa1e033b122dd58f3b495d2846d532f784ec8f6fc9ae022031dc12155c02e7f588c9f6d975a79c9c26cf341c1c88578cd6a87a3c01f6a73d6d6d6d6dc8000000000000001976a914010b7bfa8ff585b6d95f8a18a998cbd87508bd4188aca9010000000000001976a914254c6f1ce67d27ae4dafb03d2ba08318df2883c588ac000000000100'
858
963
  // full txid
859
- const txid27c8f = '27c8f37851aabc468d3dbb6bf0789dc398a602dcb897ca04e7815d939d621595'
964
+ const txid27c8f = '27c8f37851aabc468d3dbb6bf0789dc398a602dcb897ca04e7815d939d621595'
965
+
966
+ /*
967
+ txid 17d1829ba8424b97369ee8b528ee8b65d9d4b9c08d037d224a7be7c025f78f78 output 0 1196 sats
968
+ txid 9426201003b01e5bb66e9240a1cc337174238e816a25a06ba532fa67af4457d7 output 0 1197 sats
969
+ txid 509f5ef79c18504fdfe21e67608f56bacb8d8a200634e46f60a9dc8430dcd6e9 output 0 1198 sats
970
+ txid 877734db8eb917d0e2174a4bdddc085b34f67cedd6b94628c4feb1b979a2b2c5 output 0 1199 sats
971
+ txid 37abad7168d47d4e107d0f9f96813a4feef6ea346482fce554c3fade85d45409 output 0 1200 sats
972
+ txid d786db393ec7f1315bee2cbd3aa47634394f57c861feed8c075563308f14c237 output 0 1568 sats
973
+ txid a7d4cac391f5b3d8dedaacb3cd8b5afac54df2d8e043ae0452abcfe190686494 output 17 1132 sats
974
+ */
975
+ const txid17d182 = '17d1829ba8424b97369ee8b528ee8b65d9d4b9c08d037d224a7be7c025f78f78'
976
+ const beef17d182_0 = '0100beef01feaff60c000c02fd660302788ff725c0e77b4a227d038dc0b9d4d9658bee28b5e89e36974b42a89b82d117fd670300fe2b9c31ae10a15803f91f63df0cf6a5b6771db9dd831e5bc37a36460a1ffa0701fdb201009bf0e3a5854c4b4c8d62c0ade2754ae1bbc0f16a3e49251ed964486b8dc387f101d80003ae2ec810744139e9989e4f36d8f5ebb160981481e68647890d11e3dda971fa016d0081efcddc14858ead1f427f235d2b21c8de43e33bfeb155ed79f666dd4dd490c2013700a6a847f6e264cfa64c0b6aad6b74912c63c6fc89728be813d104abd2724b98a5011a0062c7d393b9518b05d2d0891ae8c2cd840c93e051d2944bb8e651c976301c4e50010c008d8ee79c1a042cf43f4dcd880aeb4ff7f54552cd3a4c9a40c72cc16ed6847cee01070050194d11d12eddf5e56926b28778f0f64ad54e79eb4df4ce7834ddfc57bf882d010200bd66a0cdbb0af933166931b1050dd108e02d76cff19afd614e681fcbdf1be0740100008b36364159630b47b4c27f6e1e056e820b0aa350ab5dde2520fc1753129b628c010100cab620ac48bb55de40c8c5ddb62bbc44c907fbf5d882326d427f4cd4b34334800101001163b76c2c6824c07a53af2ea86da1b1d946340d9377d3b9b84560e074aea3a6010100000001d75744af67fa32a56ba0256a818e23747133cca140926eb65b1eb00310202694000000006a473044022027137acdeb7583e966a9e8825c9f5edb909b2fd46d40e6ee83846d164529630e0220644e82a46d813fac2836650ce832cdba7eb46b6c134982b36e84f5f08a921dbb412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001ac040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac000000000100'
977
+ const beef17d182_4 = '0100beef01fedcee0c000d02f8020954d485defac354e5fc826434eaf6ee4f3a81969f0f7d104e7dd46871adab37f900fa225908f222f8f7f291731a10e93cdf410189ca01adc688edf5577552aa8ff2017d006cfea8ad6ace7ecce9bbbe729eb9dca25f9be064b150d290588c3364867ee112013f00bfbacc8bfdda7f734c160fcfee972dd9182aede73290367aab62b5ce242e5655011e007a0eb94f2bcf98489247a01551a843d9cf1e091522c32f50ce347ba5057b439d010e00beb857ba543d2bd199e68c63a480c73b83660eca27d2ce105355741f27017876010600d5fb1ea8440e41b4ffde093dd53d6ed741c4fe4fe66ff43f6a4eb9af3c6ddebb010200169db3be130a9e8aff1dbff39abee477d3a224cb3e118c39794bb7ba427b707501000083cd0ce2d2734835c45331cd06f38556cf4e07634d599f59692ad9980175a7f201010022f4a629e99ca5838f56f87b050bdefa9b9f621c08745cb27b941d9c6750d99a01010032b90a4148ff984b0efee309a578cb87d278142c014f1a7fd5351b3e3cd4c7da01010082dc3ea570250357fca88652ee1628928136c29eb4910b7e5627e0ffef1d958a010100f5194f41c28960f0dba669f4715229f59f59afb5d55f50a2a7556111b8da67130101009cd7f9a65014cc6fd5c0b71391d0b70eb983ea3b03a2fbd7a13150775855953405010000000237c2148f306355078cedfe61c8574f393476a43abd2cee5b31f1c73e39db86d7000000006a47304402207ee089c64fa810ad88e03f7bad4ae57c9b7de215d222a5bf52f9259d171b717a0220643d3cb4423b5b7ffcb7b2b59eb81dede0a25319aaa0228d27e59d5e238c35a2412102641b5a033ce332a9fc0f81dee7948c099b194758d8352bd5572988ac760ae2faffffffff94646890e1cfab5204ae43e0d8f24dc5fa5a8bcdb3acdaded8b3f591c3cad4a7110000006a47304402205b92eaba4a6c029385e9cf291631b98d0c7aa6851755fb44279e2b1ebd12759702206e96f3bac252f2c5cf5edf7f8828d6358baaa9fe34aa163faeb9b31aac830e47412102577e2a3877a253d9e7587c3d77a4038b4af5e51113393c2713c6d50c9da5de6effffffff03b0040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88acc8000000000000001976a914292e61672fc24a9a70cfa6f73a9af5655652629d88ac13050000000000001976a914ad2ab55d09559074e41e2a2c1c89742e13bee03088ac00000000010001000000010954d485defac354e5fc826434eaf6ee4f3a81969f0f7d104e7dd46871adab37000000006b4830450221009c67deb38aa6b5ed8bfa140e9464e1b1ee2908a296bb70e3d164725254491c6a02207b595dcced1136c276edfc1e5e5901279cf6f0a6432e4c1a908468262e7d9543412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001af040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac00000000000100000001c5b2a279b9b1fec42846b9d6ed7cf6345b08dcdd4b4a17e2d017b98edb347787000000006b483045022100c0992a684be99a07bc87e03bf88701acefc033c1aa57cf4a3b15253e64c8962a022031d5b3af1684a1a1a201eb16fc1fbca8522ae16b9c0d340e8bec48eeea1fe458412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001ae040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac00000000000100000001e9d6dc3084dca9606fe43406208a8dcbba568f60671ee2df4f50189cf75e9f50000000006b483045022100c9c8baa777fe09c1310788614d4eba88b6027d39eada18a5a79c6a33e55919c302205222961b1f63dddd425cd418571797cb3a68aa59ac37331c0fc61fc9de66c34d412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001ad040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac00000000000100000001d75744af67fa32a56ba0256a818e23747133cca140926eb65b1eb00310202694000000006a473044022027137acdeb7583e966a9e8825c9f5edb909b2fd46d40e6ee83846d164529630e0220644e82a46d813fac2836650ce832cdba7eb46b6c134982b36e84f5f08a921dbb412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001ac040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac0000000000'
978
+ const txid942620 = '9426201003b01e5bb66e9240a1cc337174238e816a25a06ba532fa67af4457d7'
979
+ const beef942620_0 = '0100beef01fe07ef0c0008022802d75744af67fa32a56ba0256a818e23747133cca140926eb65b1eb00310202694290089598873694014f9706b6074a1b349fcf59df875e58a0cf3a8900c6e5830f33601150036fffb4b882171c594a27d37c5d7827cc706811c3e0acd152e0f3ee1c7ac98f9010b002b3b5c611cbea3bbdf9e99646cb23e80b62fc2dc2e619656157296878d46d8b30104002877b6b97c95fd55c0584d0ccf683c9ad6c60be146dd73605b93ef042da648a0010300d1d01ac14f19d974f8131b9cdb38ae2abcf5403d5c3f0d2772a1c995c136356b01000017ece09932fa7c6d464c633df200003c2328beb26290c7b9b1333eb84c90c544010100cd4147f92f226bc39354a78209db1f521ffd8720e3fcc9c942ee1dd535513d0f0101005daeb3f84528a58700c58c8479f29cd627b07c198602b6c08033340262c0c607010100000001e9d6dc3084dca9606fe43406208a8dcbba568f60671ee2df4f50189cf75e9f50000000006b483045022100c9c8baa777fe09c1310788614d4eba88b6027d39eada18a5a79c6a33e55919c302205222961b1f63dddd425cd418571797cb3a68aa59ac37331c0fc61fc9de66c34d412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001ad040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac000000000100'
980
+ const txid509f5e = '509f5ef79c18504fdfe21e67608f56bacb8d8a200634e46f60a9dc8430dcd6e9'
981
+ const beef509f5e_0 = '0100beef01fe05ef0c0009029800fb09f12f5632f7af6409432fc914d1e68fc499270fa29f3b3c43fb00ebeb8ed09902e9d6dc3084dca9606fe43406208a8dcbba568f60671ee2df4f50189cf75e9f50014d00ccb4f6ea2fabdfaf86b0e3837cc702ddde19cb5112e680f00832330b03be1d290127000480365544cd4dca6262cc25567f241d650339773f3f9ef1b9a25b9a034945c30112004ab6bdfecf74af1da1e0574671d06aef63dcde80133d1a6c46beda2ad08abc15010800ce75abf88ee3de3e2f89be0462e1cb0b9470b67947b9880e941bb6565a44bc86010500940d69edc69fc8694485452b961b8df8a2e0c1ff7bdbfeee1e6855542a7e7cd10103002217abcbd7e5431fffc0f52624ee3cf659e39734ae5335d5d196193f7f6e39ab01000082d3a59ca910a1c6fc1f06ab2a5ea80ffee7a39d9a987c85701cfaaf604219f80101008bed85ed818e9d69147000e8687b2efed77e5ee0073e5a2640033715747bd0c2010100000001c5b2a279b9b1fec42846b9d6ed7cf6345b08dcdd4b4a17e2d017b98edb347787000000006b483045022100c0992a684be99a07bc87e03bf88701acefc033c1aa57cf4a3b15253e64c8962a022031d5b3af1684a1a1a201eb16fc1fbca8522ae16b9c0d340e8bec48eeea1fe458412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001ae040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac000000000100'
982
+ const txid877734 = '877734db8eb917d0e2174a4bdddc085b34f67cedd6b94628c4feb1b979a2b2c5'
983
+ const beef877734_0 = '0100beef01fedfee0c000e02fd202900eeebd665d8893249c90dabbd50266fb7e0f125dd8ee087a3c8089debf09499d4fd212902c5b2a279b9b1fec42846b9d6ed7cf6345b08dcdd4b4a17e2d017b98edb34778701fd911400b32e903076f6869d4e0a9fa2d00f5c8bad598d3ede5b4bafef88debdb1d3de6901fd490a000a79573c8eb909d30c4e9653f90ea2869e44220d551da81cc96913b2fcd734aa01fd2505003d6db6b39d889be1535a478da0c3428e8107872ac55f0fcfdd2225b6a402d1dc01fd9302008ae31e7e4d4e766814ea11b337b5b51f5d807737f802c44c8d0d75a583a61bde01fd480100e962a171fcc0e79bf55b1433d79564f8d3b2622ecbcb84c9ede792d353e0afb901a5007d0a8d8a3a2f0eb209e82f5e80ef6c4835b7d07c85d6ac8c613cc8754a47bd79015300b070576d92310f1e08a12581ecea4d1d4de48beb15ff7e42eddc1b957fa2909d012800544a58cac6b4dd78cebd84178dc1572693552fec5777a055dad0068bd67aec6101150025674a887fff026640fe9851cbffb37bd8e02780c4f6c0476ed3cee4e4884c2f010b0023aa2b01e8a59c3b79976918d95ae6a044ace2264e3690f9846f452499c0cbad01040078033c2619a3b115bfc71d09addf13e288660be1841cf6769ffb9924aca246970103004d2d6ae3e2c02703b773c49af789b7372b5b71bf92b49e2dc4b921880df984540100003b86537a85e5047596a482ac2dba30d1798b26d200c08a2fb19d8d75b2c2a1b50101000000010954d485defac354e5fc826434eaf6ee4f3a81969f0f7d104e7dd46871adab37000000006b4830450221009c67deb38aa6b5ed8bfa140e9464e1b1ee2908a296bb70e3d164725254491c6a02207b595dcced1136c276edfc1e5e5901279cf6f0a6432e4c1a908468262e7d9543412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001af040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac000000000100'
984
+ const txid37abad = '37abad7168d47d4e107d0f9f96813a4feef6ea346482fce554c3fade85d45409'
985
+ const beef37abad_0 = '0100beef01fedcee0c000d02f8020954d485defac354e5fc826434eaf6ee4f3a81969f0f7d104e7dd46871adab37f900fa225908f222f8f7f291731a10e93cdf410189ca01adc688edf5577552aa8ff2017d006cfea8ad6ace7ecce9bbbe729eb9dca25f9be064b150d290588c3364867ee112013f00bfbacc8bfdda7f734c160fcfee972dd9182aede73290367aab62b5ce242e5655011e007a0eb94f2bcf98489247a01551a843d9cf1e091522c32f50ce347ba5057b439d010e00beb857ba543d2bd199e68c63a480c73b83660eca27d2ce105355741f27017876010600d5fb1ea8440e41b4ffde093dd53d6ed741c4fe4fe66ff43f6a4eb9af3c6ddebb010200169db3be130a9e8aff1dbff39abee477d3a224cb3e118c39794bb7ba427b707501000083cd0ce2d2734835c45331cd06f38556cf4e07634d599f59692ad9980175a7f201010022f4a629e99ca5838f56f87b050bdefa9b9f621c08745cb27b941d9c6750d99a01010032b90a4148ff984b0efee309a578cb87d278142c014f1a7fd5351b3e3cd4c7da01010082dc3ea570250357fca88652ee1628928136c29eb4910b7e5627e0ffef1d958a010100f5194f41c28960f0dba669f4715229f59f59afb5d55f50a2a7556111b8da67130101009cd7f9a65014cc6fd5c0b71391d0b70eb983ea3b03a2fbd7a13150775855953401010000000237c2148f306355078cedfe61c8574f393476a43abd2cee5b31f1c73e39db86d7000000006a47304402207ee089c64fa810ad88e03f7bad4ae57c9b7de215d222a5bf52f9259d171b717a0220643d3cb4423b5b7ffcb7b2b59eb81dede0a25319aaa0228d27e59d5e238c35a2412102641b5a033ce332a9fc0f81dee7948c099b194758d8352bd5572988ac760ae2faffffffff94646890e1cfab5204ae43e0d8f24dc5fa5a8bcdb3acdaded8b3f591c3cad4a7110000006a47304402205b92eaba4a6c029385e9cf291631b98d0c7aa6851755fb44279e2b1ebd12759702206e96f3bac252f2c5cf5edf7f8828d6358baaa9fe34aa163faeb9b31aac830e47412102577e2a3877a253d9e7587c3d77a4038b4af5e51113393c2713c6d50c9da5de6effffffff03b0040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88acc8000000000000001976a914292e61672fc24a9a70cfa6f73a9af5655652629d88ac13050000000000001976a914ad2ab55d09559074e41e2a2c1c89742e13bee03088ac000000000100'
@@ -0,0 +1,132 @@
1
+ import { OverlayGASPRemote } from '../Engine'
2
+ import { GASPInitialRequest, GASPInitialResponse, GASPNode } from '@bsv/gasp'
3
+
4
+ global.fetch = jest.fn(async () => {
5
+ await Promise.resolve({
6
+ ok: true,
7
+ status: 200,
8
+ json: async () => await Promise.resolve({})
9
+ })
10
+ }
11
+ ) as jest.Mock
12
+
13
+ describe('OverlayGASPRemote', () => {
14
+ let overlayRemote: OverlayGASPRemote
15
+
16
+ beforeEach(() => {
17
+ overlayRemote = new OverlayGASPRemote('http://example.com');
18
+ (fetch as jest.Mock).mockClear()
19
+ })
20
+
21
+ describe('getInitialResponse', () => {
22
+ it('should send a request and return a valid response', async () => {
23
+ const mockRequest: GASPInitialRequest = { version: 1, since: 0 }
24
+ const mockResponse: GASPInitialResponse = {
25
+ UTXOList: [{ txid: 'txid1', outputIndex: 0 }],
26
+ since: 1234567890
27
+ };
28
+
29
+ (fetch as jest.Mock).mockResolvedValue({
30
+ ok: true,
31
+ status: 200,
32
+ json: jest.fn().mockResolvedValue(mockResponse)
33
+ })
34
+
35
+ const response = await overlayRemote.getInitialResponse(mockRequest)
36
+
37
+ expect(fetch).toHaveBeenCalledWith('http://example.com/requestSyncResponse', {
38
+ method: 'POST',
39
+ headers: { 'Content-Type': 'application/json' },
40
+ body: JSON.stringify(mockRequest)
41
+ })
42
+ expect(response).toEqual(mockResponse)
43
+ })
44
+
45
+ it('should throw an error if the response status is not OK', async () => {
46
+ const mockRequest: GASPInitialRequest = { version: 1, since: 0 };
47
+
48
+ (fetch as jest.Mock).mockResolvedValue({
49
+ ok: false,
50
+ status: 500
51
+ })
52
+
53
+ await expect(overlayRemote.getInitialResponse(mockRequest)).rejects.toThrow('HTTP error! Status: 500')
54
+ })
55
+
56
+ it('should throw an error if the response format is invalid', async () => {
57
+ const mockRequest: GASPInitialRequest = { version: 1, since: 0 }
58
+ const invalidResponse = { invalid: 'data' };
59
+
60
+ (fetch as jest.Mock).mockResolvedValue({
61
+ ok: true,
62
+ status: 200,
63
+ json: jest.fn().mockResolvedValue(invalidResponse)
64
+ })
65
+
66
+ await expect(overlayRemote.getInitialResponse(mockRequest)).rejects.toThrow('Invalid response format')
67
+ })
68
+ })
69
+
70
+ describe('requestNode', () => {
71
+ it('should send a request and return a valid GASPNode', async () => {
72
+ const graphID = 'graphID1'
73
+ const txid = 'txid1'
74
+ const outputIndex = 0
75
+ const metadata = true
76
+ const mockResponse: GASPNode = {
77
+ graphID,
78
+ rawTx: 'rawTxData',
79
+ outputIndex,
80
+ proof: 'proofData',
81
+ txMetadata: 'txMetadata',
82
+ outputMetadata: 'outputMetadata',
83
+ inputs: {}
84
+ };
85
+
86
+ (fetch as jest.Mock).mockResolvedValue({
87
+ ok: true,
88
+ status: 200,
89
+ json: jest.fn().mockResolvedValue(mockResponse)
90
+ })
91
+
92
+ const response = await overlayRemote.requestNode(graphID, txid, outputIndex, metadata)
93
+
94
+ expect(fetch).toHaveBeenCalledWith('http://example.com/requestForeignGASPNode', {
95
+ method: 'POST',
96
+ headers: { 'Content-Type': 'application/json' },
97
+ body: JSON.stringify({ graphID, txid, outputIndex, metadata })
98
+ })
99
+ expect(response).toEqual(mockResponse)
100
+ })
101
+
102
+ it('should throw an error if the response status is not OK', async () => {
103
+ const graphID = 'graphID1'
104
+ const txid = 'txid1'
105
+ const outputIndex = 0
106
+ const metadata = true;
107
+
108
+ (fetch as jest.Mock).mockResolvedValue({
109
+ ok: false,
110
+ status: 500
111
+ })
112
+
113
+ await expect(overlayRemote.requestNode(graphID, txid, outputIndex, metadata)).rejects.toThrow('HTTP error! Status: 500')
114
+ })
115
+
116
+ it('should throw an error if the response format is invalid', async () => {
117
+ const graphID = 'graphID1'
118
+ const txid = 'txid1'
119
+ const outputIndex = 0
120
+ const metadata = true
121
+ const invalidResponse = { invalid: 'data' };
122
+
123
+ (fetch as jest.Mock).mockResolvedValue({
124
+ ok: true,
125
+ status: 200,
126
+ json: jest.fn().mockResolvedValue(invalidResponse)
127
+ })
128
+
129
+ await expect(overlayRemote.requestNode(graphID, txid, outputIndex, metadata)).rejects.toThrow('Invalid response format')
130
+ })
131
+ })
132
+ })
@@ -0,0 +1,166 @@
1
+ import { GraphNode, OverlayGASPStorage } from '../Engine'
2
+ import { Transaction, MerklePath } from '@bsv/sdk'
3
+ import { GASPNode } from '@bsv/gasp'
4
+
5
+ describe('OverlayGASPStorage', () => {
6
+ let overlayStorage: OverlayGASPStorage
7
+ let mockEngine: any
8
+
9
+ beforeEach(() => {
10
+ mockEngine = { storage: { findUTXOsForTopic: jest.fn() }, managers: {} }
11
+ overlayStorage = new OverlayGASPStorage('test-topic', mockEngine)
12
+ })
13
+
14
+ describe('appendToGraph', () => {
15
+ it('should append a new node to an empty graph', async () => {
16
+ const mockTx = {
17
+ rawTx: '001122',
18
+ outputIndex: 0,
19
+ graphID: 'txid123.0'
20
+ }
21
+
22
+ // Use actual Transaction implementation
23
+ const parsedTx = Transaction.fromHex(mockTx.rawTx)
24
+
25
+ await overlayStorage.appendToGraph(mockTx)
26
+
27
+ expect(Object.keys(overlayStorage.temporaryGraphNodeRefs).length).toBe(1)
28
+ expect(overlayStorage.temporaryGraphNodeRefs['txid123.0'].txid).toBe(parsedTx.id('hex'))
29
+ })
30
+
31
+ it('throws error when max nodes are exceeded', async () => {
32
+ overlayStorage.maxNodesInGraph = 1
33
+ overlayStorage.temporaryGraphNodeRefs['txid123.0'] = { txid: 'txid123', children: [] } as GraphNode
34
+
35
+ const mockTx = {
36
+ rawTx: '334455',
37
+ outputIndex: 1,
38
+ graphID: 'txid234.1'
39
+ }
40
+
41
+ await expect(overlayStorage.appendToGraph(mockTx)).rejects.toThrow('The max number of nodes in transaction graph has been reached!')
42
+ })
43
+ })
44
+
45
+ describe('findKnownUTXOs', () => {
46
+ it('should return known UTXOs since a given timestamp', async () => {
47
+ const mockUTXOs = [{ txid: 'txid1', outputIndex: 0 }, { txid: 'txid2', outputIndex: 1 }]
48
+ mockEngine.storage.findUTXOsForTopic.mockResolvedValue(mockUTXOs)
49
+
50
+ const result = await overlayStorage.findKnownUTXOs(1234567890)
51
+
52
+ expect(result).toEqual(mockUTXOs)
53
+ expect(mockEngine.storage.findUTXOsForTopic).toHaveBeenCalledWith('test-topic', 1234567890)
54
+ })
55
+
56
+ it('should handle errors correctly', async () => {
57
+ mockEngine.storage.findUTXOsForTopic.mockRejectedValue(new Error('Database error'))
58
+
59
+ await expect(overlayStorage.findKnownUTXOs(1234567890)).rejects.toThrow('Database error')
60
+ })
61
+ })
62
+
63
+ describe('hydrateGASPNode', () => {
64
+ it('should throw an error', async () => {
65
+ await expect(overlayStorage.hydrateGASPNode('graphID', 'txid', 0, false)).rejects.toThrow('GASP node hydration Not supported!')
66
+ })
67
+ })
68
+
69
+ describe('findNeededInputs', () => {
70
+ // TODO: Write more complicated test cases
71
+ it('should return inputs needed for further verification when no proof is present', async () => {
72
+ const mockTx: GASPNode = {
73
+ rawTx: '001122',
74
+ proof: undefined,
75
+ graphID: 'txid123.0',
76
+ outputIndex: 0
77
+ }
78
+
79
+ const parsedTx = {
80
+ inputs: [{ sourceTXID: 'inputTxid1', sourceOutputIndex: 0 }],
81
+ toBEEF: jest.fn(),
82
+ id: jest.fn().mockReturnValue('txid123')
83
+ }
84
+ Transaction.fromHex = jest.fn().mockReturnValue(parsedTx)
85
+
86
+ const result = await overlayStorage.findNeededInputs(mockTx)
87
+
88
+ expect(result).toEqual({
89
+ requestedInputs: { 'inputTxid1.0': { metadata: false } }
90
+ })
91
+ })
92
+
93
+ it('should return inputs needed for further verification when proof is present', async () => {
94
+ const mockTx: GASPNode = {
95
+ rawTx: '001122',
96
+ proof: 'someproof',
97
+ graphID: 'txid123.0',
98
+ outputIndex: 0
99
+ }
100
+
101
+ const parsedTx = {
102
+ inputs: [{ sourceTXID: 'neededTxid', sourceOutputIndex: 1 }],
103
+ toBEEF: jest.fn(),
104
+ id: jest.fn().mockReturnValue('txid123'),
105
+ merklePath: {}
106
+ }
107
+ Transaction.fromHex = jest.fn().mockReturnValue(parsedTx)
108
+ MerklePath.fromHex = jest.fn().mockReturnValue(parsedTx.merklePath)
109
+
110
+ mockEngine.managers['test-topic'] = {
111
+ identifyAdmissibleOutputs: jest.fn().mockResolvedValue({
112
+ outputsToAdmit: []
113
+ }),
114
+ identifyNeededInputs: jest.fn().mockResolvedValue([{ txid: 'neededTxid', outputIndex: 1 }])
115
+ }
116
+
117
+ const result = await overlayStorage.findNeededInputs(mockTx)
118
+
119
+ expect(result).toEqual({
120
+ requestedInputs: { 'neededTxid.1': { metadata: false } }
121
+ })
122
+ })
123
+ })
124
+
125
+ describe('discardGraph', () => {
126
+ it('should discard the graph and its nodes', async () => {
127
+ overlayStorage.temporaryGraphNodeRefs['txid123.0'] = {
128
+ txid: 'txid123',
129
+ time: Date.now(),
130
+ graphID: 'txid123.0',
131
+ rawTx: 'rawTxData',
132
+ outputIndex: 0,
133
+ children: [],
134
+ parent: undefined
135
+ } as GraphNode
136
+
137
+ overlayStorage.temporaryGraphNodeRefs['txid124.0'] = {
138
+ txid: 'txid124',
139
+ time: Date.now(),
140
+ graphID: 'txid123.0',
141
+ rawTx: 'rawTxData',
142
+ outputIndex: 1,
143
+ children: [],
144
+ parent: {
145
+ txid: 'txid123',
146
+ time: Date.now(),
147
+ graphID: 'txid123.0',
148
+ rawTx: 'rawTxData',
149
+ outputIndex: 0,
150
+ children: []
151
+ } as GraphNode
152
+ } as GraphNode
153
+
154
+ await overlayStorage.discardGraph('txid123.0')
155
+
156
+ expect(overlayStorage.temporaryGraphNodeRefs['txid123.0']).toBeUndefined()
157
+ expect(overlayStorage.temporaryGraphNodeRefs['txid124.0']).toBeUndefined()
158
+ })
159
+ })
160
+
161
+ it('should handle non-existent graphID', async () => {
162
+ await overlayStorage.discardGraph('nonexistent.0')
163
+
164
+ expect(Object.keys(overlayStorage.temporaryGraphNodeRefs).length).toBe(0)
165
+ })
166
+ })
@@ -35,6 +35,14 @@ export interface Storage {
35
35
  */
36
36
  findOutputsForTransaction: (txid: string) => Promise<Output[]>
37
37
 
38
+ /**
39
+ * Finds current UTXOs that have been admitted into a given topic
40
+ * @param topic - The topic for which we want to find Unspent Transaction Outputs (UTXOs).
41
+ * @param since - Optional parameter indicating the minimum date (timestamp? block height?) to retrieve matching UTXOs from. TODO: We need to decide about which time format to use for synchronization.
42
+ * @returns A promise that resolves to an array of matching UTXOs.
43
+ */
44
+ findUTXOsForTopic: (topic: string, since?: number) => Promise<Output[]>
45
+
38
46
  /**
39
47
  * Deletes an output from storage
40
48
  * @param txid — The TXID of the output to delete
@@ -56,6 +56,32 @@ export class KnexStorage implements Storage {
56
56
  }))
57
57
  }
58
58
 
59
+ async findUTXOsForTopic(topic: string, since?: number): Promise<Output[]> {
60
+ const query = this.knex('outputs').where({ topic, spent: false })
61
+
62
+ // TODO: Create new migration for outputs that adds either created_at or block height tagging for proven outpoints if necessary.
63
+ // if (since !== undefined) {
64
+ // const sinceDate = new Date(since).toISOString()
65
+ // await query.andWhere('created_at', '>=', sinceDate)
66
+ // }
67
+
68
+ const outputs = await query.select(
69
+ 'txid', 'outputIndex', 'outputScript', 'topic', 'satoshis', 'beef', 'outputsConsumed', 'spent', 'consumedBy'
70
+ )
71
+
72
+ if (outputs === undefined || outputs.length === 0) {
73
+ return []
74
+ }
75
+
76
+ return outputs.map(output => ({
77
+ ...output,
78
+ outputScript: [...output.outputScript],
79
+ beef: [...output.beef],
80
+ spent: Boolean(output.spent),
81
+ outputsConsumed: JSON.parse(output.outputsConsumed),
82
+ consumedBy: JSON.parse(output.consumedBy)
83
+ }))
84
+ }
59
85
 
60
86
  async deleteOutput(txid: string, outputIndex: number, topic: string): Promise<void> {
61
87
  await this.knex('outputs').where({