@bsv/overlay 0.1.0-alpha.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 (119) hide show
  1. package/LICENSE.txt +28 -0
  2. package/README.md +220 -0
  3. package/dist/cjs/mod.js +43 -0
  4. package/dist/cjs/mod.js.map +1 -0
  5. package/dist/cjs/package.json +53 -0
  6. package/dist/cjs/src/AdmittanceInstructions.js +3 -0
  7. package/dist/cjs/src/AdmittanceInstructions.js.map +1 -0
  8. package/dist/cjs/src/Engine.js +361 -0
  9. package/dist/cjs/src/Engine.js.map +1 -0
  10. package/dist/cjs/src/LookupAnswer.js +3 -0
  11. package/dist/cjs/src/LookupAnswer.js.map +1 -0
  12. package/dist/cjs/src/LookupFormula.js +3 -0
  13. package/dist/cjs/src/LookupFormula.js.map +1 -0
  14. package/dist/cjs/src/LookupQuestion.js +3 -0
  15. package/dist/cjs/src/LookupQuestion.js.map +1 -0
  16. package/dist/cjs/src/LookupService.js +3 -0
  17. package/dist/cjs/src/LookupService.js.map +1 -0
  18. package/dist/cjs/src/Output.js +3 -0
  19. package/dist/cjs/src/Output.js.map +1 -0
  20. package/dist/cjs/src/STEAK.js +3 -0
  21. package/dist/cjs/src/STEAK.js.map +1 -0
  22. package/dist/cjs/src/TaggedBEEF.js +3 -0
  23. package/dist/cjs/src/TaggedBEEF.js.map +1 -0
  24. package/dist/cjs/src/TopicManager.js +3 -0
  25. package/dist/cjs/src/TopicManager.js.map +1 -0
  26. package/dist/cjs/src/storage/Storage.js +3 -0
  27. package/dist/cjs/src/storage/Storage.js.map +1 -0
  28. package/dist/cjs/src/storage/knex/KnexStorage.js +76 -0
  29. package/dist/cjs/src/storage/knex/KnexStorage.js.map +1 -0
  30. package/dist/cjs/src/storage/knex/all-migrations.js +11 -0
  31. package/dist/cjs/src/storage/knex/all-migrations.js.map +1 -0
  32. package/dist/cjs/src/storage/knex/migrations/2024-05-18-001-initial.js +33 -0
  33. package/dist/cjs/src/storage/knex/migrations/2024-05-18-001-initial.js.map +1 -0
  34. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -0
  35. package/dist/esm/mod.js +17 -0
  36. package/dist/esm/mod.js.map +1 -0
  37. package/dist/esm/src/AdmittanceInstructions.js +2 -0
  38. package/dist/esm/src/AdmittanceInstructions.js.map +1 -0
  39. package/dist/esm/src/Engine.js +357 -0
  40. package/dist/esm/src/Engine.js.map +1 -0
  41. package/dist/esm/src/LookupAnswer.js +2 -0
  42. package/dist/esm/src/LookupAnswer.js.map +1 -0
  43. package/dist/esm/src/LookupFormula.js +2 -0
  44. package/dist/esm/src/LookupFormula.js.map +1 -0
  45. package/dist/esm/src/LookupQuestion.js +2 -0
  46. package/dist/esm/src/LookupQuestion.js.map +1 -0
  47. package/dist/esm/src/LookupService.js +2 -0
  48. package/dist/esm/src/LookupService.js.map +1 -0
  49. package/dist/esm/src/Output.js +2 -0
  50. package/dist/esm/src/Output.js.map +1 -0
  51. package/dist/esm/src/STEAK.js +2 -0
  52. package/dist/esm/src/STEAK.js.map +1 -0
  53. package/dist/esm/src/TaggedBEEF.js +2 -0
  54. package/dist/esm/src/TaggedBEEF.js.map +1 -0
  55. package/dist/esm/src/TopicManager.js +2 -0
  56. package/dist/esm/src/TopicManager.js.map +1 -0
  57. package/dist/esm/src/storage/Storage.js +2 -0
  58. package/dist/esm/src/storage/Storage.js.map +1 -0
  59. package/dist/esm/src/storage/knex/KnexStorage.js +74 -0
  60. package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -0
  61. package/dist/esm/src/storage/knex/all-migrations.js +9 -0
  62. package/dist/esm/src/storage/knex/all-migrations.js.map +1 -0
  63. package/dist/esm/src/storage/knex/migrations/2024-05-18-001-initial.js +28 -0
  64. package/dist/esm/src/storage/knex/migrations/2024-05-18-001-initial.js.map +1 -0
  65. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -0
  66. package/dist/types/mod.d.ts +14 -0
  67. package/dist/types/mod.d.ts.map +1 -0
  68. package/dist/types/src/AdmittanceInstructions.d.ts +14 -0
  69. package/dist/types/src/AdmittanceInstructions.d.ts.map +1 -0
  70. package/dist/types/src/Engine.d.ts +92 -0
  71. package/dist/types/src/Engine.d.ts.map +1 -0
  72. package/dist/types/src/LookupAnswer.d.ts +15 -0
  73. package/dist/types/src/LookupAnswer.d.ts.map +1 -0
  74. package/dist/types/src/LookupFormula.d.ts +25 -0
  75. package/dist/types/src/LookupFormula.d.ts.map +1 -0
  76. package/dist/types/src/LookupQuestion.d.ts +15 -0
  77. package/dist/types/src/LookupQuestion.d.ts.map +1 -0
  78. package/dist/types/src/LookupService.d.ts +52 -0
  79. package/dist/types/src/LookupService.d.ts.map +1 -0
  80. package/dist/types/src/Output.d.ts +30 -0
  81. package/dist/types/src/Output.d.ts.map +1 -0
  82. package/dist/types/src/STEAK.d.ts +10 -0
  83. package/dist/types/src/STEAK.d.ts.map +1 -0
  84. package/dist/types/src/TaggedBEEF.d.ts +11 -0
  85. package/dist/types/src/TaggedBEEF.d.ts.map +1 -0
  86. package/dist/types/src/TopicManager.d.ts +27 -0
  87. package/dist/types/src/TopicManager.d.ts.map +1 -0
  88. package/dist/types/src/storage/Storage.d.ts +66 -0
  89. package/dist/types/src/storage/Storage.d.ts.map +1 -0
  90. package/dist/types/src/storage/knex/KnexStorage.d.ts +24 -0
  91. package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -0
  92. package/dist/types/src/storage/knex/all-migrations.d.ts +10 -0
  93. package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -0
  94. package/dist/types/src/storage/knex/migrations/2024-05-18-001-initial.d.ts +4 -0
  95. package/dist/types/src/storage/knex/migrations/2024-05-18-001-initial.d.ts.map +1 -0
  96. package/dist/types/tsconfig.types.tsbuildinfo +1 -0
  97. package/docs/API.md +621 -0
  98. package/docs/README.md +8 -0
  99. package/docs/concepts/README.md +4 -0
  100. package/docs/examples/README.md +5 -0
  101. package/docs/examples/gs-wip.md +105 -0
  102. package/docs/internal/README.md +4 -0
  103. package/mod.ts +18 -0
  104. package/package.json +78 -0
  105. package/src/AdmittanceInstructions.ts +14 -0
  106. package/src/Engine.ts +419 -0
  107. package/src/LookupAnswer.ts +14 -0
  108. package/src/LookupFormula.ts +26 -0
  109. package/src/LookupQuestion.ts +15 -0
  110. package/src/LookupService.ts +52 -0
  111. package/src/Output.ts +29 -0
  112. package/src/STEAK.ts +10 -0
  113. package/src/TaggedBEEF.ts +10 -0
  114. package/src/TopicManager.ts +23 -0
  115. package/src/__tests/Engine.test.ts +792 -0
  116. package/src/storage/Storage.ts +72 -0
  117. package/src/storage/knex/KnexStorage.ts +90 -0
  118. package/src/storage/knex/all-migrations.ts +14 -0
  119. package/src/storage/knex/migrations/2024-05-18-001-initial.ts +30 -0
@@ -0,0 +1,792 @@
1
+ import Engine from '../Engine'
2
+ import LookupService from '../LookupService'
3
+ import TopicManager from '../TopicManager'
4
+ import { AdmittanceInstructions } from '../AdmittanceInstructions'
5
+ import Storage from '../storage/Storage'
6
+ import { Transaction } from '@bsv/sdk'
7
+ import { Output } from '../Output'
8
+
9
+ const mockChainTracker = {
10
+ isValidRootForHeight: jest.fn(async () => true)
11
+ }
12
+
13
+ const BRC62Hex = '0100beef01fe636d0c0007021400fe507c0c7aa754cef1f7889d5fd395cf1f785dd7de98eed895dbedfe4e5bc70d1502ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e010b00bc4ff395efd11719b277694cface5aa50d085a0bb81f613f70313acd28cf4557010400574b2d9142b8d28b61d88e3b2c3f44d858411356b49a28a4643b6d1a6a092a5201030051a05fc84d531b5d250c23f4f886f6812f9fe3f402d61607f977b4ecd2701c19010000fd781529d58fc2523cf396a7f25440b409857e7e221766c57214b1d38c7b481f01010062f542f45ea3660f86c013ced80534cb5fd4c19d66c56e7e8c5d4bf2d40acc5e010100b121e91836fd7cd5102b654e9f72f3cf6fdbfd0b161c53a9c54b12c841126331020100000001cd4e4cac3c7b56920d1e7655e7e260d31f29d9a388d04910f1bbd72304a79029010000006b483045022100e75279a205a547c445719420aa3138bf14743e3f42618e5f86a19bde14bb95f7022064777d34776b05d816daf1699493fcdf2ef5a5ab1ad710d9c97bfb5b8f7cef3641210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000001000100000001ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000000'
14
+ const exampleTX = Transaction.fromHexBEEF(BRC62Hex)
15
+
16
+ const exampleBeef = exampleTX.toBEEF()
17
+ const exampleTXID = exampleTX.id('hex') as string
18
+ const examplePreviousTXID = '3ecead27a44d013ad1aae40038acbb1883ac9242406808bb4667c15b4f164eac'
19
+ let mockTopicManager: TopicManager, mockLookupService: LookupService, mockStorageEngine: Storage
20
+ const mockOutput: Output = {
21
+ txid: exampleTXID,
22
+ outputIndex: 0,
23
+ outputScript: exampleTX.outputs[0].lockingScript.toBinary(),
24
+ topic: 'hello',
25
+ satoshis: exampleTX.outputs[0].satoshis as number,
26
+ beef: exampleBeef,
27
+ spent: false,
28
+ outputsConsumed: [],
29
+ consumedBy: []
30
+ }
31
+
32
+ describe('BSV Overlay Services Engine', () => {
33
+ beforeEach(() => {
34
+ jest.clearAllMocks()
35
+
36
+ mockTopicManager = {
37
+ identifyAdmissibleOutputs: jest.fn(async (): Promise<AdmittanceInstructions> => ({
38
+ outputsToAdmit: [0],
39
+ coinsToRetain: []
40
+ })),
41
+ getDocumentation: async () => 'Topical Documentation',
42
+ getMetaData: async () => ({ name: 'Mock Manager', shortDescription: 'Mock Short Manager Description' })
43
+ }
44
+
45
+ mockLookupService = {
46
+ outputAdded: jest.fn(),
47
+ outputSpent: jest.fn(),
48
+ lookup: jest.fn(),
49
+ outputDeleted: jest.fn(),
50
+ getDocumentation: async () => 'Service Documentation',
51
+ getMetaData: async () => ({ name: 'Mock Service', shortDescription: 'Mock Short Service Description' })
52
+ }
53
+
54
+ mockStorageEngine = {
55
+ doesAppliedTransactionExist: jest.fn(async () => false),
56
+ insertAppliedTransaction: jest.fn(),
57
+ insertOutput: jest.fn(),
58
+ findOutput: jest.fn(async () => null),
59
+ markUTXOAsSpent: jest.fn(),
60
+ updateConsumedBy: jest.fn(),
61
+ deleteOutput: jest.fn()
62
+ }
63
+ })
64
+ describe('submit', () => {
65
+ it('Throws an error if the user submits to a topic that is not supported', async () => {
66
+ const engine = new Engine(
67
+ {
68
+ Hello: mockTopicManager
69
+ },
70
+ {},
71
+ mockStorageEngine,
72
+ mockChainTracker
73
+ )
74
+ await expect(engine.submit({
75
+ beef: exampleBeef,
76
+ topics: ['hello']
77
+ })).rejects.toHaveProperty('message', 'This server does not support this topic: hello')
78
+ })
79
+ it('Verifies the BEEF for the provided transaction', async () => {
80
+ const engine = new Engine(
81
+ {
82
+ Hello: mockTopicManager
83
+ },
84
+ {},
85
+ mockStorageEngine,
86
+ mockChainTracker
87
+ )
88
+ await engine.submit({
89
+ beef: exampleBeef,
90
+ topics: ['Hello']
91
+ })
92
+ expect(mockChainTracker.isValidRootForHeight).toHaveBeenCalledWith('bb6f640cc4ee56bf38eb5a1969ac0c16caa2d3d202b22bf3735d10eec0ca6e00', 814435)
93
+ })
94
+ it('Throws an error if an invalid envelope is provided', async () => {
95
+ const engine = new Engine(
96
+ {
97
+ Hello: mockTopicManager
98
+ },
99
+ {},
100
+ mockStorageEngine,
101
+ mockChainTracker
102
+ )
103
+ mockChainTracker.isValidRootForHeight.mockReturnValueOnce(Promise.resolve(false))
104
+ await expect(engine.submit({
105
+ beef: exampleBeef,
106
+ topics: ['Hello']
107
+ })).rejects.toHaveProperty('message', 'Verification failed because the input at index 0 of transaction 3ecead27a44d013ad1aae40038acbb1883ac9242406808bb4667c15b4f164eac is missing an associated source transaction. This source transaction is required for transaction verification because there is no merkle proof for the transaction spending a UTXO it contains.')
108
+ })
109
+ describe('For each topic being processed', () => {
110
+ it('Checks for duplicate transactions', async () => {
111
+ const engine = new Engine(
112
+ {
113
+ Hello: mockTopicManager
114
+ },
115
+ {},
116
+ mockStorageEngine,
117
+ mockChainTracker
118
+ )
119
+ await engine.submit({
120
+ beef: exampleBeef,
121
+ topics: ['Hello']
122
+ })
123
+ expect(mockStorageEngine.doesAppliedTransactionExist).toHaveBeenCalledWith({ txid: exampleTXID, topic: 'Hello' })
124
+ })
125
+ it('Does not process the output if the transaction was already applied to the topic', async () => {
126
+ mockStorageEngine.doesAppliedTransactionExist = jest.fn(async () => true)
127
+ const engine = new Engine(
128
+ {
129
+ Hello: mockTopicManager
130
+ },
131
+ {},
132
+ mockStorageEngine,
133
+ mockChainTracker
134
+ )
135
+ await engine.submit({
136
+ beef: exampleBeef,
137
+ topics: ['Hello']
138
+ })
139
+ expect(mockStorageEngine.doesAppliedTransactionExist).toReturnWith(Promise.resolve(true))
140
+ expect(mockStorageEngine.findOutput).not.toHaveBeenCalled()
141
+ expect(mockStorageEngine.markUTXOAsSpent).not.toHaveBeenCalled()
142
+ expect(mockStorageEngine.insertOutput).not.toHaveBeenCalled()
143
+ })
144
+ describe('For each input of the transaction', () => {
145
+ it('Acquires the appropriate previous topical UTXOs from the storage engine', async () => {
146
+ // Mock findUTXO to return a UTXO
147
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
148
+ const engine = new Engine(
149
+ {
150
+ Hello: mockTopicManager
151
+ },
152
+ {},
153
+ mockStorageEngine,
154
+ mockChainTracker
155
+ )
156
+
157
+ // Submit the utxo
158
+ await engine.submit({
159
+ beef: exampleBeef,
160
+ topics: ['Hello']
161
+ })
162
+ expect(mockStorageEngine.findOutput).toHaveBeenCalled()
163
+ })
164
+ it('Includes the appropriate previous topical UTXOs when they are returned from the storage engine', async () => {
165
+ // Mock findUTXO to return a UTXO
166
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
167
+ const engine = new Engine(
168
+ {
169
+ Hello: mockTopicManager
170
+ },
171
+ {},
172
+ mockStorageEngine,
173
+ mockChainTracker
174
+ )
175
+
176
+ // Submit the utxo
177
+ await engine.submit({
178
+ beef: exampleBeef,
179
+ topics: ['Hello']
180
+ })
181
+ expect(mockStorageEngine.findOutput).toHaveBeenCalled()
182
+ expect(mockStorageEngine.markUTXOAsSpent).toHaveBeenCalledWith(exampleTXID, 0, 'Hello')
183
+ })
184
+ })
185
+ it('Identifies admissible outputs with the appropriate topic manager', async () => {
186
+ // Mock findUTXO to return a UTXO
187
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
188
+ const engine = new Engine(
189
+ {
190
+ Hello: mockTopicManager
191
+ },
192
+ {},
193
+ mockStorageEngine,
194
+ mockChainTracker
195
+ )
196
+
197
+ // Submit the utxo
198
+ await engine.submit({
199
+ beef: exampleBeef,
200
+ topics: ['Hello']
201
+ })
202
+ expect(engine.managers.Hello.identifyAdmissibleOutputs).toHaveBeenCalledWith(exampleBeef, [0])
203
+ })
204
+ describe('When previous UTXOs were retained by the topic manager', () => {
205
+ it('Notifies all lookup services about the output being spent (not deleted, see the comment about this in deleteUTXODeep)', async () => {
206
+ // Mock findUTXO to return a UTXO
207
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
208
+ const engine = new Engine(
209
+ {
210
+ Hello: mockTopicManager
211
+ },
212
+ {
213
+ Hello: mockLookupService
214
+ },
215
+ mockStorageEngine,
216
+ mockChainTracker
217
+ )
218
+
219
+ // Submit the utxo
220
+ await engine.submit({
221
+ beef: exampleBeef,
222
+ topics: ['Hello']
223
+ })
224
+ expect(engine.lookupServices.Hello.outputSpent).toHaveBeenCalledWith(exampleTXID, 0, 'Hello')
225
+ })
226
+ })
227
+ describe('When previous UTXOs were not retained by the topic manager', () => {
228
+ it('Marks the UTXO as stale, deleting all stale UTXOs by calling deleteUTXODeep', async () => {
229
+ // Mock findUTXO to return a UTXO
230
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
231
+ const engine = new Engine(
232
+ {
233
+ Hello: mockTopicManager
234
+ },
235
+ {
236
+ Hello: mockLookupService
237
+ },
238
+ mockStorageEngine,
239
+ mockChainTracker
240
+ )
241
+
242
+ // Submit the utxo
243
+ await engine.submit({
244
+ beef: exampleBeef,
245
+ topics: ['Hello']
246
+ })
247
+ // Test that previous UTXOs are deleted
248
+ expect(mockStorageEngine.deleteOutput).toHaveBeenCalledWith(exampleTXID, 0, 'hello')
249
+ expect(mockLookupService.outputDeleted).toHaveBeenCalledWith(exampleTXID, 0, 'hello')
250
+ })
251
+ it('Notifies all lookup services about the output being spent (the notification about the actual deletion will come from deleteUTXODeep)', async () => {
252
+ // Mock findUTXO to return a UTXO
253
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
254
+ const engine = new Engine(
255
+ {
256
+ Hello: mockTopicManager
257
+ },
258
+ {
259
+ Hello: mockLookupService
260
+ },
261
+ mockStorageEngine,
262
+ mockChainTracker
263
+ )
264
+
265
+ // Submit the utxo
266
+ await engine.submit({
267
+ beef: exampleBeef,
268
+ topics: ['Hello']
269
+ })
270
+ // Was the lookup service notified of the output deletion?
271
+ expect(mockLookupService.outputDeleted).toHaveBeenCalledWith(exampleTXID, 0, 'hello')
272
+ })
273
+ })
274
+ it('Adds admissible UTXOs to the storage engine', async () => {
275
+ // Mock findUTXO to return a UTXO
276
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
277
+ const engine = new Engine(
278
+ {
279
+ hello: mockTopicManager
280
+ },
281
+ {
282
+ Hello: mockLookupService
283
+ },
284
+ mockStorageEngine,
285
+ mockChainTracker
286
+ )
287
+
288
+ // Submit the utxo
289
+ await engine.submit({
290
+ beef: exampleBeef,
291
+ topics: ['hello']
292
+ })
293
+ // Test the new UTXO was added
294
+ expect(mockStorageEngine.insertOutput).toHaveBeenCalledWith(mockOutput)
295
+ })
296
+ it('Notifies lookup services about incoming admissible UTXOs', async () => {
297
+ // Mock findUTXO to return a UTXO
298
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
299
+ const engine = new Engine(
300
+ {
301
+ Hello: mockTopicManager
302
+ },
303
+ {
304
+ Hello: mockLookupService
305
+ },
306
+ mockStorageEngine,
307
+ mockChainTracker
308
+ )
309
+
310
+ // Submit the utxo
311
+ await engine.submit({
312
+ beef: exampleBeef,
313
+ topics: ['Hello']
314
+ })
315
+ // Test the lookup service was notified of the new UTXO
316
+ expect(mockLookupService.outputAdded).toHaveBeenCalledWith(
317
+ exampleTXID,
318
+ 0,
319
+ exampleTX.outputs[0].lockingScript,
320
+ 'Hello'
321
+ )
322
+ })
323
+ describe('For each consumed UTXO', () => {
324
+ it('Finds the UTXO', async () => {
325
+ // Mock findUTXO to return a UTXO
326
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
327
+ mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
328
+ return {
329
+ outputsToAdmit: [0],
330
+ coinsToRetain: [0]
331
+ }
332
+ })
333
+ const engine = new Engine(
334
+ {
335
+ Hello: mockTopicManager
336
+ },
337
+ {
338
+ Hello: mockLookupService
339
+ },
340
+ mockStorageEngine,
341
+ mockChainTracker
342
+ )
343
+
344
+ // Submit the utxo
345
+ await engine.submit({
346
+ beef: exampleBeef,
347
+ topics: ['Hello']
348
+ })
349
+
350
+ // Test a storage engine lookup happens for the utxo consumed
351
+ expect(mockStorageEngine.findOutput).toHaveBeenCalledWith(examplePreviousTXID, 0, 'Hello')
352
+ })
353
+ it('Updates the UTXO to reflect that it is now additionally consumed by the newly-created UTXOs', async () => {
354
+ // Mock findUTXO to return a UTXO
355
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
356
+ mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
357
+ return {
358
+ outputsToAdmit: [0],
359
+ coinsToRetain: [0]
360
+ }
361
+ })
362
+ const engine = new Engine(
363
+ {
364
+ Hello: mockTopicManager
365
+ },
366
+ {
367
+ Hello: mockLookupService
368
+ },
369
+ mockStorageEngine,
370
+ mockChainTracker
371
+ )
372
+
373
+ // Submit the utxo
374
+ await engine.submit({
375
+ beef: exampleBeef,
376
+ topics: ['Hello']
377
+ })
378
+
379
+ // Test the consumedBy data is updated
380
+ expect(mockStorageEngine.updateConsumedBy).toHaveBeenCalledWith(examplePreviousTXID, 0, 'Hello', [{
381
+ txid: exampleTXID,
382
+ outputIndex: 0
383
+ }])
384
+ })
385
+ })
386
+ it('Inserts a new applied transaction to avoid de-duplication', async () => {
387
+ // Mock findUTXO to return a UTXO
388
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
389
+ mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
390
+ return {
391
+ outputsToAdmit: [0],
392
+ coinsToRetain: [0]
393
+ }
394
+ })
395
+ const engine = new Engine(
396
+ {
397
+ Hello: mockTopicManager
398
+ },
399
+ {
400
+ Hello: mockLookupService
401
+ },
402
+ mockStorageEngine,
403
+ mockChainTracker
404
+ )
405
+
406
+ // Submit the utxo
407
+ await engine.submit({
408
+ beef: exampleBeef,
409
+ topics: ['Hello']
410
+ })
411
+
412
+ // Test the tx is inserted
413
+ expect(mockStorageEngine.insertAppliedTransaction).toHaveBeenCalledWith({
414
+ txid: exampleTXID,
415
+ topic: 'Hello'
416
+ })
417
+ })
418
+ it('Returns a correct set of admitted topics and outputs', async () => {
419
+ // Mock findUTXO to return a UTXO
420
+ mockStorageEngine.insertOutput = jest.fn()
421
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
422
+ mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
423
+ return {
424
+ outputsToAdmit: [0],
425
+ coinsToRetain: [0]
426
+ }
427
+ })
428
+ const engine = new Engine(
429
+ {
430
+ Hello: mockTopicManager
431
+ },
432
+ {
433
+ Hello: mockLookupService
434
+ },
435
+ mockStorageEngine,
436
+ mockChainTracker
437
+ )
438
+
439
+ // Submit the utxo
440
+ const results = await engine.submit({
441
+ beef: exampleBeef,
442
+ topics: ['Hello']
443
+ })
444
+
445
+ // Test the correct outputs are admitted
446
+ expect(results).toEqual({
447
+ Hello: {
448
+ outputsToAdmit: [0],
449
+ coinsToRetain: [0]
450
+ }
451
+ })
452
+ })
453
+ })
454
+
455
+ describe('lookup', () => {
456
+ it('Throws an error if no lookup service has this provider name', async () => {
457
+ const engine = new Engine(
458
+ {
459
+ Hello: mockTopicManager
460
+ },
461
+ {
462
+ Hello: mockLookupService
463
+ },
464
+ mockStorageEngine,
465
+ mockChainTracker
466
+ )
467
+
468
+ // Perform a lookup request
469
+ await expect(engine.lookup({
470
+ service: 'HelloWorld',
471
+ query: { name: 'Bob' }
472
+ })).rejects.toThrow()
473
+ })
474
+ it('Calls the lookup function from the lookup service', async () => {
475
+ // TODO: Make the default storage engine return something...?
476
+ mockLookupService.lookup = jest.fn(async () => [{ txid: exampleTXID, outputIndex: 0 }])
477
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
478
+ const engine = new Engine(
479
+ {
480
+ Hello: mockTopicManager
481
+ },
482
+ {
483
+ Hello: mockLookupService
484
+ },
485
+ mockStorageEngine,
486
+ mockChainTracker
487
+ )
488
+
489
+ // Perform a lookup request
490
+ await engine.lookup({
491
+ service: 'Hello',
492
+ query: { name: 'Bob' }
493
+ })
494
+ expect(mockLookupService.lookup).toHaveBeenCalledWith({
495
+ service: 'Hello',
496
+ query: { name: 'Bob' }
497
+ })
498
+ })
499
+ describe('For each returned result', () => {
500
+ it('Finds the identified UTXO by its txid and vout', async () => {
501
+ // TODO: Make the default storage engine return something...?
502
+ mockLookupService.lookup = jest.fn(async () => [{
503
+ txid: 'mockTXID',
504
+ outputIndex: 0,
505
+ history: undefined
506
+ }])
507
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
508
+ const engine = new Engine(
509
+ {
510
+ Hello: mockTopicManager
511
+ },
512
+ {
513
+ Hello: mockLookupService
514
+ },
515
+ mockStorageEngine,
516
+ mockChainTracker
517
+ )
518
+
519
+ // Perform a lookup request
520
+ await engine.lookup({
521
+ service: 'Hello',
522
+ query: { name: 'Bob' }
523
+ })
524
+ expect(mockStorageEngine.findOutput).toHaveBeenCalledWith(
525
+ "mockTXID", 0, undefined, false
526
+ )
527
+ })
528
+ it('Calls getUTXOHistory with the correct UTXO and history parameters', async () => {
529
+ mockLookupService.lookup = jest.fn(async () => [{
530
+ txid: 'mockTXID',
531
+ outputIndex: 0,
532
+ history: undefined
533
+ }])
534
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
535
+ const engine = new Engine(
536
+ {
537
+ Hello: mockTopicManager
538
+ },
539
+ {
540
+ Hello: mockLookupService
541
+ },
542
+ mockStorageEngine,
543
+ mockChainTracker
544
+ )
545
+ engine.getUTXOHistory = jest.fn(async () => {
546
+ return mockOutput
547
+ })
548
+
549
+ // Perform a lookup request
550
+ await engine.lookup({
551
+ service: 'Hello',
552
+ query: { name: 'Bob' }
553
+ })
554
+ expect(engine.getUTXOHistory).toHaveBeenCalledWith(
555
+ mockOutput,
556
+ undefined,
557
+ 0
558
+ )
559
+ })
560
+ })
561
+ it('Returns the correct set of hydrated results', async () => {
562
+ mockLookupService.lookup = jest.fn(async () => [{
563
+ txid: 'mockTXID',
564
+ outputIndex: 0,
565
+ history: undefined
566
+ }])
567
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
568
+ const engine = new Engine(
569
+ {
570
+ Hello: mockTopicManager
571
+ },
572
+ {
573
+ Hello: mockLookupService
574
+ },
575
+ mockStorageEngine,
576
+ mockChainTracker
577
+ )
578
+ engine.getUTXOHistory = jest.fn(async () => {
579
+ return mockOutput
580
+ })
581
+
582
+ // Perform a lookup request
583
+ const results = await engine.lookup({
584
+ service: 'Hello',
585
+ query: { name: 'Bob' }
586
+ })
587
+ expect(results).toEqual({
588
+ outputs: [{
589
+ beef: mockOutput.beef,
590
+ outputIndex: mockOutput.outputIndex
591
+ }], type: 'output-list'
592
+ })
593
+ })
594
+ })
595
+ describe('getUTXOHistory', () => {
596
+ it('Returns the given output if there is no history selector', async () => {
597
+ // Already tested above
598
+ return true
599
+ })
600
+ it('Invokes the history selector function with the correct data', async () => {
601
+ const mockedHistorySelector = jest.fn(async (beef, outputIndex, currentDepth) => {
602
+ if (currentDepth !== 2) {
603
+ return true
604
+ }
605
+ return false
606
+ })
607
+ mockLookupService.lookup = jest.fn(async () => [{
608
+ txid: 'mockTXID',
609
+ outputIndex: 0,
610
+ history: mockedHistorySelector
611
+ }])
612
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
613
+ const engine = new Engine(
614
+ {
615
+ Hello: mockTopicManager
616
+ },
617
+ {
618
+ Hello: mockLookupService
619
+ },
620
+ mockStorageEngine,
621
+ mockChainTracker
622
+ )
623
+
624
+ // Perform a lookup request
625
+ await engine.lookup({
626
+ service: 'Hello',
627
+ query: { name: 'Bob' }
628
+ })
629
+ expect(mockedHistorySelector).toHaveBeenCalledWith(
630
+ mockOutput.beef,
631
+ mockOutput.outputIndex,
632
+ 0
633
+ )
634
+ expect(mockedHistorySelector).toReturnWith(Promise.resolve(true))
635
+ })
636
+ it('Returns undefined if history should not be traversed', async () => {
637
+ const mockedHistorySelector = jest.fn(async (beef, outputIndex, currentDepth) => {
638
+ return false
639
+ })
640
+ mockLookupService.lookup = jest.fn(async () => [{
641
+ txid: 'mockTXID',
642
+ outputIndex: 0,
643
+ history: mockedHistorySelector
644
+ }])
645
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
646
+ const engine = new Engine(
647
+ {
648
+ Hello: mockTopicManager
649
+ },
650
+ {
651
+ Hello: mockLookupService
652
+ },
653
+ mockStorageEngine,
654
+ mockChainTracker
655
+ )
656
+
657
+ // Perform a lookup request
658
+ const results = await engine.lookup({
659
+ service: 'Hello',
660
+ query: { name: 'Bob' }
661
+ })
662
+ expect(mockedHistorySelector).toHaveBeenCalledWith(
663
+ mockOutput.beef,
664
+ mockOutput.outputIndex,
665
+ 0
666
+ )
667
+ expect(results).toEqual({
668
+ outputs: [],
669
+ type: "output-list"
670
+ })
671
+ })
672
+ it('Returns undefined if the history selector is a number, and less than the current depth', async () => {
673
+ mockLookupService.lookup = jest.fn(async () => [{
674
+ txid: 'mockTXID',
675
+ outputIndex: 0,
676
+ history: -1
677
+ }])
678
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
679
+ const engine = new Engine(
680
+ {
681
+ Hello: mockTopicManager
682
+ },
683
+ {
684
+ Hello: mockLookupService
685
+ },
686
+ mockStorageEngine,
687
+ mockChainTracker
688
+ )
689
+
690
+ // Perform a lookup request
691
+ const results = await engine.lookup({
692
+ service: 'Hello',
693
+ query: { name: 'Bob' }
694
+ })
695
+ expect(results).toEqual({
696
+ outputs: [],
697
+ type: "output-list"
698
+ })
699
+ })
700
+ 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 () => {
701
+ mockLookupService.lookup = jest.fn(async () => [{
702
+ txid: 'mockTXID',
703
+ outputIndex: 0,
704
+ history: 1
705
+ }])
706
+ mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
707
+ const engine = new Engine(
708
+ {
709
+ Hello: mockTopicManager
710
+ },
711
+ {
712
+ Hello: mockLookupService
713
+ },
714
+ mockStorageEngine,
715
+ mockChainTracker
716
+ )
717
+
718
+ // Perform a lookup request
719
+ const results = await engine.lookup({
720
+ service: 'Hello',
721
+ query: { name: 'Bob' }
722
+ })
723
+
724
+ expect(results).toEqual({
725
+ outputs: [{
726
+ beef: mockOutput.beef,
727
+ outputIndex: mockOutput.outputIndex
728
+ }], type: 'output-list'
729
+ })
730
+ })
731
+ // it('Traversing history, calls findOutput with any output consumed by this UTXO', async () => {
732
+ // mockLookupService.lookup = jest.fn(async () => [{
733
+ // txid: 'mockTXID',
734
+ // outputIndex: 0,
735
+ // history: 1
736
+ // }])
737
+ // mockStorageEngine.findOutput = jest.fn(async () => ({
738
+ // ...mockOutput,
739
+ // outputsConsumed: [{
740
+ // txid: examplePreviousTXID,
741
+ // outputIndex: 0
742
+ // }]
743
+ // }))
744
+ // const engine = new Engine(
745
+ // {
746
+ // Hello: mockTopicManager
747
+ // },
748
+ // {
749
+ // Hello: mockLookupService
750
+ // },
751
+ // mockStorageEngine,
752
+ // mockChainTracker
753
+ // )
754
+
755
+ // // Perform a lookup request
756
+ // await engine.lookup({
757
+ // service: 'Hello',
758
+ // query: { name: 'Bob' }
759
+ // })
760
+
761
+ // expect(mockStorageEngine.findOutput).toHaveBeenCalledWith()
762
+ // })
763
+ // it('Returns the correct envelope based on the history traversal process', async () => {
764
+ // // TODO: Come up with some test data for a simple case, but different than the above code.
765
+ // })
766
+ // it('Returns the correct envelope based on the history traversal process for a more complex multi-layer multi-output graph', async () => {
767
+ // // TODO: Come up with some test data to test the history traversal process better
768
+ // })
769
+ })
770
+ // describe('deleteUTXODeep', () => {
771
+ // it('Finds UTXO by ID if no output was provided', async () => {
772
+
773
+ // })
774
+ // it('Finds UTXO by TXID and VOUT if no ID was provided', async () => {
775
+
776
+ // })
777
+ // it('Throws an error if no output can be found', async () => {
778
+
779
+ // })
780
+ // describe('When no more UTXOs are consumed by this UTXO', () => {
781
+ // it('Deletes the UTXO by ID from the storage engine', async () => {
782
+
783
+ // })
784
+ // it('Notifies all lookup services about the deletion of the UTXO', async () => {
785
+
786
+ // })
787
+ // })
788
+ // it('For each UTXO that this UTXO consumes, finds the consumed UTXO by its ID and removes reference to this one', async () => {
789
+
790
+ // })
791
+ })
792
+ })