@bsv/overlay 2.2.1 → 2.3.0

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 (151) hide show
  1. package/LICENSE.txt +57 -27
  2. package/README.md +102 -269
  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 +41 -13
  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 +379 -0
  19. package/dist/cjs/src/Engine.d.ts.map +1 -0
  20. package/dist/cjs/src/Engine.js +425 -418
  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 +32 -30
  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.map +1 -1
  62. package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.d.ts +4 -0
  63. package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.d.ts.map +1 -0
  64. package/dist/cjs/src/storage/knex/migrations/2024-07-18-001-indexes.js.map +1 -1
  65. package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.d.ts +4 -0
  66. package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.d.ts.map +1 -0
  67. package/dist/cjs/src/storage/knex/migrations/2025-05-28-001-enlarge.js.map +1 -1
  68. package/dist/cjs/src/storage/knex/migrations/2025-06-25-001-gasp-pagination-support.d.ts +4 -0
  69. package/dist/cjs/src/storage/knex/migrations/2025-06-25-001-gasp-pagination-support.d.ts.map +1 -0
  70. package/dist/cjs/src/storage/knex/migrations/2025-06-25-001-gasp-pagination-support.js.map +1 -1
  71. package/dist/cjs/src/storage/knex/migrations/2025-07-22-001-fix-score-column-type.d.ts +4 -0
  72. package/dist/cjs/src/storage/knex/migrations/2025-07-22-001-fix-score-column-type.d.ts.map +1 -0
  73. package/dist/cjs/src/storage/knex/migrations/2025-07-22-001-fix-score-column-type.js.map +1 -1
  74. package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.d.ts +9 -0
  75. package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.d.ts.map +1 -0
  76. package/dist/cjs/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.js.map +1 -1
  77. package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts +4 -0
  78. package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.d.ts.map +1 -0
  79. package/dist/cjs/src/storage/knex/migrations/2026-05-29-001-brc136-basm.js.map +1 -1
  80. package/dist/esm/mod.js +2 -1
  81. package/dist/esm/mod.js.map +1 -1
  82. package/dist/esm/src/BASMRemote.js.map +1 -1
  83. package/dist/esm/src/Engine.js +404 -396
  84. package/dist/esm/src/Engine.js.map +1 -1
  85. package/dist/esm/src/GASP/OverlayGASPRemote.js +2 -2
  86. package/dist/esm/src/GASP/OverlayGASPRemote.js.map +1 -1
  87. package/dist/esm/src/GASP/OverlayGASPStorage.js +21 -18
  88. package/dist/esm/src/GASP/OverlayGASPStorage.js.map +1 -1
  89. package/dist/esm/src/SafeLog.js +40 -0
  90. package/dist/esm/src/SafeLog.js.map +1 -0
  91. package/dist/esm/src/storage/knex/KnexStorage.js.map +1 -1
  92. package/dist/types/mod.d.ts +5 -4
  93. package/dist/types/mod.d.ts.map +1 -1
  94. package/dist/types/src/BASMRemote.d.ts.map +1 -1
  95. package/dist/types/src/Engine.d.ts +18 -1
  96. package/dist/types/src/Engine.d.ts.map +1 -1
  97. package/dist/types/src/GASP/OverlayGASPRemote.d.ts +2 -2
  98. package/dist/types/src/GASP/OverlayGASPRemote.d.ts.map +1 -1
  99. package/dist/types/src/GASP/OverlayGASPStorage.d.ts +3 -1
  100. package/dist/types/src/GASP/OverlayGASPStorage.d.ts.map +1 -1
  101. package/dist/types/src/SafeLog.d.ts +14 -0
  102. package/dist/types/src/SafeLog.d.ts.map +1 -0
  103. package/dist/types/src/storage/knex/KnexStorage.d.ts.map +1 -1
  104. package/dist/types/src/storage/knex/all-migrations.d.ts.map +1 -1
  105. package/package.json +85 -28
  106. package/dist/cjs/tsconfig.cjs.tsbuildinfo +0 -1
  107. package/dist/esm/tsconfig.esm.tsbuildinfo +0 -1
  108. package/dist/types/tsconfig.types.tsbuildinfo +0 -1
  109. package/docs/API.md +0 -944
  110. package/docs/BRC-136-BASM.md +0 -327
  111. package/docs/README.md +0 -16
  112. package/docs/Synchronization.md +0 -212
  113. package/docs/concepts/00-overview.md +0 -85
  114. package/docs/concepts/01-best-practices.md +0 -202
  115. package/docs/concepts/02-query-performance.md +0 -345
  116. package/docs/concepts/03-database-monitoring.md +0 -211
  117. package/docs/concepts/04-pagination-example.md +0 -186
  118. package/docs/concepts/05-recommendations-summary.md +0 -158
  119. package/docs/concepts/README.md +0 -20
  120. package/docs/examples/README.md +0 -15
  121. package/docs/examples/gs-wip.md +0 -111
  122. package/docs/internal/README.md +0 -11
  123. package/mod.ts +0 -40
  124. package/src/Advertisement.ts +0 -8
  125. package/src/Advertiser.ts +0 -41
  126. package/src/BASM.ts +0 -208
  127. package/src/BASMRemote.ts +0 -54
  128. package/src/Engine.ts +0 -2194
  129. package/src/GASP/OverlayGASPRemote.ts +0 -108
  130. package/src/GASP/OverlayGASPStorage.ts +0 -384
  131. package/src/LookupFormula.ts +0 -31
  132. package/src/LookupService.ts +0 -136
  133. package/src/Output.ts +0 -31
  134. package/src/TopicManager.ts +0 -40
  135. package/src/__tests/BASM.test.ts +0 -44
  136. package/src/__tests/BASMChain.test.ts +0 -295
  137. package/src/__tests/Engine.test.ts +0 -1381
  138. package/src/__tests/OverlayGASPRemote.test.ts +0 -135
  139. package/src/__tests/OverlayGASPStorage.test.ts +0 -205
  140. package/src/storage/Storage.ts +0 -260
  141. package/src/storage/knex/KnexStorage.ts +0 -651
  142. package/src/storage/knex/all-migrations.ts +0 -32
  143. package/src/storage/knex/migrations/2024-05-18-001-initial.ts +0 -30
  144. package/src/storage/knex/migrations/2024-07-10-001-block-height.ts +0 -15
  145. package/src/storage/knex/migrations/2024-07-17-001-transactions.ts +0 -64
  146. package/src/storage/knex/migrations/2024-07-18-001-indexes.ts +0 -30
  147. package/src/storage/knex/migrations/2025-05-28-001-enlarge.ts +0 -42
  148. package/src/storage/knex/migrations/2025-06-25-001-gasp-pagination-support.ts +0 -46
  149. package/src/storage/knex/migrations/2025-07-22-001-fix-score-column-type.ts +0 -24
  150. package/src/storage/knex/migrations/2025-11-11-001-utxo-lookup-index.ts +0 -18
  151. package/src/storage/knex/migrations/2026-05-29-001-brc136-basm.ts +0 -123
@@ -1,1381 +0,0 @@
1
- // Note: References Engine from dist due to issues with compiled code references in Engine.ts
2
- import { Engine } from '../../dist/cjs/src/Engine.js'
3
- import { LookupService } from '../LookupService'
4
- import { TopicManager } from '../TopicManager'
5
- import { Storage } from '../storage/Storage'
6
- import { Transaction, Utils, TaggedBEEF, AdmittanceInstructions, STEAK } from '@bsv/sdk'
7
- import { Output } from '../Output'
8
- import { SyncConfiguration } from '../SyncConfiguration'
9
- import { Advertiser } from '../Advertiser'
10
-
11
- const mockChainTracker = {
12
- isValidRootForHeight: jest.fn(async () => true),
13
- currentHeight: jest.fn(async () => 800000)
14
- }
15
-
16
- // Call the method that would normally trigger syncAdvertisements
17
- const engineSubmit = jest.fn(async (taggedBEEF: TaggedBEEF, onSteakReady: any, mode?: string): Promise<STEAK> => {
18
- const result: STEAK = { tm_helloworld: { outputsToAdmit: [], coinsToRetain: [], coinsRemoved: [] } }
19
- return result
20
- })
21
-
22
- const BRC62Hex = '0100beef01fe636d0c0007021400fe507c0c7aa754cef1f7889d5fd395cf1f785dd7de98eed895dbedfe4e5bc70d1502ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e010b00bc4ff395efd11719b277694cface5aa50d085a0bb81f613f70313acd28cf4557010400574b2d9142b8d28b61d88e3b2c3f44d858411356b49a28a4643b6d1a6a092a5201030051a05fc84d531b5d250c23f4f886f6812f9fe3f402d61607f977b4ecd2701c19010000fd781529d58fc2523cf396a7f25440b409857e7e221766c57214b1d38c7b481f01010062f542f45ea3660f86c013ced80534cb5fd4c19d66c56e7e8c5d4bf2d40acc5e010100b121e91836fd7cd5102b654e9f72f3cf6fdbfd0b161c53a9c54b12c841126331020100000001cd4e4cac3c7b56920d1e7655e7e260d31f29d9a388d04910f1bbd72304a79029010000006b483045022100e75279a205a547c445719420aa3138bf14743e3f42618e5f86a19bde14bb95f7022064777d34776b05d816daf1699493fcdf2ef5a5ab1ad710d9c97bfb5b8f7cef3641210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013e660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000001000100000001ac4e164f5bc16746bb0868404292ac8318bbac3800e4aad13a014da427adce3e000000006a47304402203a61a2e931612b4bda08d541cfb980885173b8dcf64a3471238ae7abcd368d6402204cbf24f04b9aa2256d8901f0ed97866603d2be8324c2bfb7a37bf8fc90edd5b441210263e2dee22b1ddc5e11f6fab8bcd2378bdd19580d640501ea956ec0e786f93e76ffffffff013c660000000000001976a9146bfd5c7fbe21529d45803dbcf0c87dd3c71efbc288ac0000000000'
23
- const exampleTX = Transaction.fromHexBEEF(BRC62Hex)
24
-
25
- const exampleBeef = exampleTX.toBEEF()
26
- const exampleTXID = exampleTX.id('hex')
27
- const examplePreviousTXID = '3ecead27a44d013ad1aae40038acbb1883ac9242406808bb4667c15b4f164eac'
28
- let mockTopicManager: TopicManager, mockLookupService: LookupService, mockStorageEngine: Storage
29
- const mockOutput: Output = {
30
- txid: exampleTXID,
31
- outputIndex: 0,
32
- outputScript: exampleTX.outputs[0].lockingScript.toBinary(),
33
- topic: 'hello',
34
- satoshis: exampleTX.outputs[0].satoshis,
35
- beef: exampleBeef,
36
- spent: false,
37
- outputsConsumed: [],
38
- consumedBy: [],
39
- score: 1234567890
40
- }
41
-
42
- const invalidHostingUrls = [
43
- 'http://example.com', // Invalid: http
44
- 'https://localhost:3000', // Invalid: localhost
45
- 'https://192.168.1.1', // Invalid: internal private IP
46
- 'https://127.0.0.1', // Invalid: loopback IP
47
- 'https://0.0.0.0', // Invalid: non-routable IP
48
- 'http://172.16.0.1', // Invalid: private IP
49
- '[::1]'
50
- ]
51
-
52
- const validHostingUrls = [
53
- 'https://example.com', // Valid: public URL
54
- 'https://8.8.8.8', // Valid: public routable IP
55
- 'https://255.255.255.255' // Valid: public routable IP
56
- ]
57
-
58
- const mockAdvertiser: Advertiser = {
59
- createAdvertisements: jest.fn(async (): Promise<TaggedBEEF> => ({ beef: [0, 1, 2], topics: [] })),
60
- revokeAdvertisements: jest.fn(async (): Promise<TaggedBEEF> => ({ beef: [0, 1, 2], topics: [] })),
61
- findAllAdvertisements: jest.fn(async () => []),
62
- parseAdvertisement: jest.fn()
63
- }
64
-
65
- describe('BSV Overlay Services Engine', () => {
66
- beforeEach(() => {
67
- jest.clearAllMocks()
68
-
69
- mockTopicManager = {
70
- identifyAdmissibleOutputs: jest.fn(async (): Promise<AdmittanceInstructions> => ({
71
- outputsToAdmit: [0],
72
- coinsToRetain: []
73
- })),
74
- getDocumentation: async () => 'Topical Documentation',
75
- getMetaData: async () => ({ name: 'Mock Manager', shortDescription: 'Mock Short Manager Description' })
76
- }
77
-
78
- mockLookupService = {
79
- outputAdmittedByTopic: jest.fn(),
80
- outputSpent: jest.fn(),
81
- lookup: jest.fn(),
82
- outputNoLongerRetainedInHistory: jest.fn(),
83
- outputEvicted: jest.fn(),
84
- admissionMode: 'locking-script',
85
- spendNotificationMode: 'none',
86
- getDocumentation: async () => 'Service Documentation',
87
- getMetaData: async () => ({ name: 'Mock Service', shortDescription: 'Mock Short Service Description' })
88
- }
89
-
90
- mockStorageEngine = {
91
- doesAppliedTransactionExist: jest.fn(async () => false),
92
- insertAppliedTransaction: jest.fn(),
93
- insertOutput: jest.fn(),
94
- findOutput: jest.fn(async () => null),
95
- findOutputsForTransaction: jest.fn(async () => []),
96
- markUTXOAsSpent: jest.fn(),
97
- updateConsumedBy: jest.fn(),
98
- updateTransactionBEEF: jest.fn(),
99
- deleteOutput: jest.fn(),
100
- findUTXOsForTopic: jest.fn(),
101
- updateLastInteraction: jest.fn(),
102
- getLastInteraction: jest.fn(async () => 0)
103
- }
104
- })
105
-
106
- it('engine.syncAdvertisements should return void when invalid hostingURL is provided', async () => {
107
- for (const url of invalidHostingUrls) {
108
- const engine = new Engine(
109
- { tm_helloworld: mockTopicManager },
110
- { ls_helloworld: mockLookupService },
111
- mockStorageEngine,
112
- mockChainTracker,
113
- url, // Invalid hostingURL
114
- ['tracker1'], // shipTrackers
115
- ['tracker2'], // slapTrackers
116
- undefined,
117
- mockAdvertiser,
118
- undefined
119
- )
120
-
121
- engine.submit = engineSubmit
122
-
123
- // Call the method that would normally trigger syncAdvertisements
124
- const result = await engine.syncAdvertisements()
125
-
126
- expect(result).toBeUndefined()
127
-
128
- // Verify that Advertiser methods are NOT called
129
- expect(mockAdvertiser.createAdvertisements).not.toHaveBeenCalled()
130
- expect(mockAdvertiser.findAllAdvertisements).not.toHaveBeenCalledWith('SHIP') // Assuming 'SHIP' is expected
131
- }
132
- })
133
-
134
- it('should allow engine.syncAdvertisements to proceed with valid hostingURLs', async () => {
135
- const mockAdvertiser: Advertiser = {
136
- createAdvertisements: jest.fn().mockResolvedValue({ tag: 'MOCK_TAG', data: Buffer.from('mock data') }),
137
- findAllAdvertisements: jest.fn().mockResolvedValue([]),
138
- revokeAdvertisements: jest.fn().mockResolvedValue({ tag: 'MOCK_REVOKE_TAG', data: Buffer.from('revocation data') }),
139
- parseAdvertisement: jest.fn().mockReturnValue({
140
- protocol: 'SHIP',
141
- topicOrServiceName: 'mock-topic',
142
- timestamp: Date.now()
143
- })
144
- }
145
-
146
- for (const url of validHostingUrls) {
147
- const engine = new Engine(
148
- { tm_helloworld: mockTopicManager },
149
- { ls_helloworld: mockLookupService },
150
- mockStorageEngine,
151
- mockChainTracker,
152
- url, // Valid hostingURL
153
- ['tracker1'], // shipTrackers
154
- ['tracker2'], // slapTrackers
155
- undefined,
156
- mockAdvertiser, // Pass the mock Advertiser
157
- undefined
158
- )
159
-
160
- engine.submit = engineSubmit
161
- await engine.syncAdvertisements()
162
- expect(engine.submit).toHaveBeenCalled()
163
-
164
- // Verify that Advertiser methods are called
165
- expect(mockAdvertiser.createAdvertisements).toHaveBeenCalled()
166
- expect(mockAdvertiser.findAllAdvertisements).toHaveBeenCalledWith('SHIP') // Assuming 'SHIP' is expected
167
- }
168
- })
169
-
170
- it('refreshes old unproven transaction proofs before eviction', async () => {
171
- const storage = {
172
- ...mockStorageEngine,
173
- findUnprovenAppliedTransactions: jest.fn(async () => [
174
- {
175
- txid: exampleTXID,
176
- topic: 'Hello',
177
- firstSeenHeight: 799000,
178
- outputs: [{ txid: exampleTXID, outputIndex: 0 }]
179
- }
180
- ])
181
- }
182
- const engine = new Engine(
183
- { tm_helloworld: mockTopicManager },
184
- { ls_helloworld: mockLookupService },
185
- storage,
186
- mockChainTracker,
187
- 'https://example.com'
188
- )
189
- const merklePath = {} as any
190
- ;(engine as any).handleNewMerkleProof = jest.fn(async () => undefined)
191
-
192
- const report = await engine.refreshUnprovenTransactionProofs({
193
- thresholdBlocks: 144,
194
- proofProvider: jest.fn(async () => ({ merklePath, blockHeight: 799900 }))
195
- })
196
-
197
- expect(report.refreshedTransactions).toBe(1)
198
- expect(report.missingProofs).toBe(0)
199
- expect((engine as any).handleNewMerkleProof).toHaveBeenCalledWith(exampleTXID, merklePath, 799900)
200
- })
201
-
202
- it('evicts provider-invalidated applied transactions', async () => {
203
- const deleteAppliedTransaction = jest.fn(async () => undefined)
204
- const deleteOutput = jest.fn(async () => undefined)
205
- const engine = new Engine(
206
- { tm_helloworld: mockTopicManager },
207
- { ls_helloworld: mockLookupService },
208
- {
209
- ...mockStorageEngine,
210
- findOutputsForTransaction: jest.fn(async () => [
211
- {
212
- ...mockOutput,
213
- topic: 'Hello'
214
- }
215
- ]),
216
- deleteOutput,
217
- deleteAppliedTransaction
218
- },
219
- mockChainTracker,
220
- 'https://example.com'
221
- )
222
-
223
- const report = await engine.evictAppliedTransaction(exampleTXID, {
224
- reason: 'DOUBLE_SPEND_ATTEMPTED'
225
- })
226
-
227
- expect(report.evictedTransactions).toBe(1)
228
- expect(report.evictedOutputs).toBe(1)
229
- expect(mockLookupService.outputEvicted).toHaveBeenCalledWith(exampleTXID, 0)
230
- expect(deleteOutput).toHaveBeenCalledWith(exampleTXID, 0, 'Hello')
231
- expect(deleteAppliedTransaction).toHaveBeenCalledWith(exampleTXID, 'Hello')
232
- })
233
-
234
- it('Uses SHIP sync configuration by default if no syncConfiguration was provided', () => {
235
- const engine = new Engine(
236
- { tm_helloworld: mockTopicManager },
237
- { ls_helloworld: mockLookupService },
238
- mockStorageEngine,
239
- mockChainTracker,
240
- undefined, // hostingURL
241
- ['tracker1'], // shipTrackers
242
- ['tracker2'], // slapTrackers
243
- undefined,
244
- undefined,
245
- undefined
246
- )
247
-
248
- expect(engine.syncConfiguration).toEqual({ tm_helloworld: 'SHIP' })
249
- })
250
-
251
- it('Does not set sync method to "SHIP" for topic managers set to false in the syncConfiguration', () => {
252
- const syncConfiguration: SyncConfiguration = { tm_helloworld: false }
253
- const engine = new Engine(
254
- { tm_helloworld: mockTopicManager },
255
- { ls_helloworld: mockLookupService },
256
- mockStorageEngine,
257
- mockChainTracker,
258
- undefined, // hostingURL
259
- ['tracker1'], // shipTrackers
260
- ['tracker2'], // slapTrackers
261
- undefined,
262
- undefined,
263
- syncConfiguration
264
- )
265
-
266
- expect(engine.syncConfiguration).toEqual({ tm_helloworld: false })
267
- })
268
-
269
- it('Combines existing trackers with provided shipTrackers and slapTrackers, ensuring no duplicates', () => {
270
- const syncConfiguration: SyncConfiguration = { tm_ship: ['existingTracker1'], tm_slap: ['existingTracker2'] }
271
- const engine = new Engine(
272
- { tm_ship: mockTopicManager, tm_slap: mockTopicManager },
273
- { ls_ship: mockLookupService, ls_slap: mockLookupService },
274
- mockStorageEngine,
275
- mockChainTracker,
276
- undefined, // hostingURL
277
- ['tracker1', 'existingTracker1'], // shipTrackers
278
- ['tracker2', 'existingTracker2'], // slapTrackers
279
- undefined,
280
- undefined,
281
- syncConfiguration
282
- )
283
-
284
- expect(engine.syncConfiguration).toEqual({
285
- tm_ship: ['existingTracker1', 'tracker1'],
286
- tm_slap: ['existingTracker2', 'tracker2']
287
- })
288
- })
289
-
290
- it('Sets undefined topic managers in syncConfiguration to sync method of "SHIP" by default', () => {
291
- const syncConfiguration: SyncConfiguration = { tm_helloworld: 'SHIP' }
292
- const engine = new Engine(
293
- { tm_helloworld: mockTopicManager, tm_ship: mockTopicManager, tm_slap: mockTopicManager },
294
- { ls_helloworld: mockLookupService, ls_ship: mockLookupService, ls_slap: mockLookupService },
295
- mockStorageEngine,
296
- mockChainTracker,
297
- undefined, // hostingURL
298
- ['tracker1'], // shipTrackers
299
- ['tracker2'], // slapTrackers
300
- undefined,
301
- undefined,
302
- syncConfiguration
303
- )
304
-
305
- expect(engine.syncConfiguration).toEqual({
306
- tm_helloworld: 'SHIP',
307
- tm_ship: ['tracker1'],
308
- tm_slap: ['tracker2']
309
- })
310
- })
311
- describe('handleNewMerkleProof tests', () => {
312
- it('persists a replacement proof when the stored BEEF is already mined', async () => {
313
- const tx = Transaction.fromHexBEEF(beef37abad0)
314
- const txid = tx.id('hex')
315
- const output: Output = {
316
- txid,
317
- outputIndex: 0,
318
- outputScript: tx.outputs[0].lockingScript.toBinary(),
319
- topic: 'hello',
320
- satoshis: tx.outputs[0].satoshis,
321
- beef: tx.toBEEF(),
322
- spent: false,
323
- outputsConsumed: [],
324
- consumedBy: [],
325
- score: Date.now()
326
- }
327
- const updateTransactionBEEF = jest.fn(async () => undefined)
328
- mockStorageEngine.findOutputsForTransaction = jest.fn(async () => [output])
329
- mockStorageEngine.updateTransactionBEEF = updateTransactionBEEF
330
- const engine = new Engine(
331
- { Hello: mockTopicManager },
332
- { Hello: mockLookupService },
333
- mockStorageEngine,
334
- mockChainTracker
335
- )
336
-
337
- if (tx.merklePath === undefined) throw new Error('improper test setup')
338
- await engine.handleNewMerkleProof(txid, tx.merklePath)
339
-
340
- expect(updateTransactionBEEF).toHaveBeenCalledWith(txid, tx.toBEEF())
341
- })
342
-
343
- it('0 simple proof', async () => {
344
- const beef = beef27c8f1
345
- const txid = txid27c8f
346
- const tx = Transaction.fromHexBEEF(beef)
347
-
348
- const output27c8fZero: Output = {
349
- txid,
350
- outputIndex: 0,
351
- outputScript: tx.outputs[0].lockingScript.toBinary(),
352
- topic: 'hello',
353
- satoshis: tx.outputs[0].satoshis,
354
- beef: tx.toBEEF(),
355
- spent: false,
356
- outputsConsumed: [],
357
- consumedBy: [],
358
- score: Date.now()
359
- }
360
-
361
- mockLookupService.lookup = jest.fn(async () => [{
362
- txid,
363
- outputIndex: 0,
364
- history: 1
365
- }])
366
- let newBEEF: number[] = []
367
- mockStorageEngine.findOutput = jest.fn(async () => output27c8fZero)
368
- mockStorageEngine.findOutputsForTransaction = jest.fn(async () => [output27c8fZero])
369
- mockStorageEngine.updateTransactionBEEF = jest.fn(async (txid: string, beef: number[]) => {
370
- newBEEF = beef
371
- })
372
- const engine = new Engine(
373
- {
374
- Hello: mockTopicManager
375
- },
376
- {
377
- Hello: mockLookupService
378
- },
379
- mockStorageEngine,
380
- mockChainTracker
381
- )
382
-
383
- const merklePath = Transaction.fromHexBEEF(beef27c8f0).merklePath
384
- if (merklePath == null) throw new Error('improper test setup')
385
- await engine.handleNewMerkleProof(txid, merklePath)
386
- expect(newBEEF.length > 0 && newBEEF.length < beef.length / 2).toBe(true)
387
- })
388
-
389
- it('1 recurse proof', async () => {
390
- const outputs: Output[] = []
391
- const findOutput = (txid: string, outputIndex: number, includeBEEF?: boolean): Output => {
392
- const i = outputs.findIndex(o => o.txid === txid && o.outputIndex === outputIndex)
393
- if (i < 0) throw new Error(`missing output ${txid} ${outputIndex}`)
394
- return outputs[i]
395
- }
396
-
397
- const addConsumingOutput = (tx: Transaction, outputIndex: number, consumes?: Output): Output => {
398
- const o: Output = {
399
- txid: tx.id('hex'),
400
- outputIndex,
401
- outputScript: tx.outputs[outputIndex].lockingScript.toBinary(),
402
- topic: 'hello',
403
- satoshis: tx.outputs[outputIndex].satoshis,
404
- beef: tx.toBEEF(),
405
- spent: false,
406
- outputsConsumed: [],
407
- consumedBy: [],
408
- score: Date.now()
409
- }
410
- if (consumes != null) {
411
- const c = findOutput(consumes.txid, consumes.outputIndex)
412
- c.consumedBy.push(o)
413
- o.outputsConsumed.push(c)
414
- }
415
- outputs.push(o)
416
- return o
417
- }
418
-
419
- mockLookupService.lookup = jest.fn(async () => [{ txid: txid17d182, outputIndex: 0, history: 1 }])
420
- const newBEEF: Record<string, string> = {}
421
- mockStorageEngine.findOutput = jest.fn(async (txid: string, outputIndex: number, topic?: string, spent?: boolean, includeBEEF?: boolean) => {
422
- return findOutput(txid, outputIndex, true)
423
- })
424
- mockStorageEngine.findOutputsForTransaction = jest.fn(async (txid: string, includeBEEF?: boolean) => {
425
- const os = outputs.filter(o => o.txid === txid)
426
- return os
427
- })
428
- mockStorageEngine.updateTransactionBEEF = jest.fn(async (txid: string, beef: number[]) => {
429
- newBEEF[txid] = Utils.toHex(beef)
430
- })
431
- const engine = new Engine({ Hello: mockTopicManager }, { Hello: mockLookupService }, mockStorageEngine, mockChainTracker)
432
-
433
- /*
434
- txid 17d1829ba8424b97369ee8b528ee8b65d9d4b9c08d037d224a7be7c025f78f78 output 0 1196 sats
435
- txid 9426201003b01e5bb66e9240a1cc337174238e816a25a06ba532fa67af4457d7 output 0 1197 sats
436
- txid 509f5ef79c18504fdfe21e67608f56bacb8d8a200634e46f60a9dc8430dcd6e9 output 0 1198 sats
437
- txid 877734db8eb917d0e2174a4bdddc085b34f67cedd6b94628c4feb1b979a2b2c5 output 0 1199 sats
438
- txid 37abad7168d47d4e107d0f9f96813a4feef6ea346482fce554c3fade85d45409 output 0 1200 sats
439
- txid d786db393ec7f1315bee2cbd3aa47634394f57c861feed8c075563308f14c237 output 0 1568 sats
440
- txid a7d4cac391f5b3d8dedaacb3cd8b5afac54df2d8e043ae0452abcfe190686494 output 17 1132 sats
441
- */
442
-
443
- const tx17d182 = Transaction.fromHexBEEF(beef17d1824)
444
- const tx942620 = tx17d182.inputs[0].sourceTransaction
445
- const tx509f5e = tx942620.inputs[0].sourceTransaction
446
- const tx877734 = tx509f5e.inputs[0].sourceTransaction
447
- const tx37abad = tx877734.inputs[0].sourceTransaction
448
-
449
- const output37abadZero = addConsumingOutput(tx37abad, 0)
450
- const output877734Zero = addConsumingOutput(tx877734, 0, output37abadZero)
451
- const output509f5eZero = addConsumingOutput(tx509f5e, 0, output877734Zero)
452
- const output942620Zero = addConsumingOutput(tx942620, 0, output509f5eZero)
453
- addConsumingOutput(tx17d182, 0, output942620Zero)
454
-
455
- const mp37abad = Transaction.fromHexBEEF(beef37abad0).merklePath
456
- await engine.handleNewMerkleProof(txid37abad, mp37abad)
457
- expect(newBEEF[`${txid37abad}`]).toBe(beef37abad0)
458
- expect(newBEEF[`${txid877734}`].length).toBeGreaterThan(beef8777340.length)
459
- expect(newBEEF[`${txid509f5e}`].length).toBeGreaterThan(beef509f5e0.length)
460
- expect(newBEEF[`${txid942620}`].length).toBeGreaterThan(beef9426200.length)
461
- expect(newBEEF[`${txid17d182}`].length).toBeGreaterThan(beef17d1820.length)
462
- expect(Object.keys(newBEEF)).toHaveLength(5)
463
-
464
- const mp877734 = Transaction.fromHexBEEF(beef8777340).merklePath
465
- await engine.handleNewMerkleProof(txid877734, mp877734)
466
- expect(newBEEF[`${txid877734}`]).toBe(beef8777340)
467
- expect(newBEEF[`${txid509f5e}`].length).toBeGreaterThan(beef509f5e0.length)
468
- expect(newBEEF[`${txid942620}`].length).toBeGreaterThan(beef9426200.length)
469
- expect(newBEEF[`${txid17d182}`].length).toBeGreaterThan(beef17d1820.length)
470
- expect(Object.keys(newBEEF)).toHaveLength(5)
471
-
472
- const mp509f5e = Transaction.fromHexBEEF(beef509f5e0).merklePath
473
- await engine.handleNewMerkleProof(txid509f5e, mp509f5e)
474
- expect(newBEEF[`${txid877734}`]).toBe(beef8777340)
475
- expect(newBEEF[`${txid509f5e}`]).toBe(beef509f5e0)
476
- expect(newBEEF[`${txid942620}`].length).toBeGreaterThan(beef9426200.length)
477
- expect(newBEEF[`${txid17d182}`].length).toBeGreaterThan(beef17d1820.length)
478
- expect(Object.keys(newBEEF)).toHaveLength(5)
479
-
480
- const mp942620 = Transaction.fromHexBEEF(beef9426200).merklePath
481
- await engine.handleNewMerkleProof(txid942620, mp942620)
482
- expect(newBEEF[`${txid877734}`]).toBe(beef8777340)
483
- expect(newBEEF[`${txid509f5e}`]).toBe(beef509f5e0)
484
- expect(newBEEF[`${txid942620}`]).toBe(beef9426200)
485
- expect(newBEEF[`${txid17d182}`].length).toBeGreaterThan(beef17d1820.length)
486
- expect(Object.keys(newBEEF)).toHaveLength(5)
487
-
488
- const mp17d182 = Transaction.fromHexBEEF(beef17d1820).merklePath
489
- await engine.handleNewMerkleProof(txid17d182, mp17d182)
490
- expect(newBEEF[`${txid877734}`]).toBe(beef8777340)
491
- expect(newBEEF[`${txid509f5e}`]).toBe(beef509f5e0)
492
- expect(newBEEF[`${txid942620}`]).toBe(beef9426200)
493
- expect(newBEEF[`${txid17d182}`]).toBe(beef17d1820)
494
- expect(Object.keys(newBEEF)).toHaveLength(5)
495
- })
496
- })
497
-
498
- describe('submit', () => {
499
- it('Throws an error if the user submits to a topic that is not supported', async () => {
500
- const engine = new Engine(
501
- {
502
- Hello: mockTopicManager
503
- },
504
- {},
505
- mockStorageEngine,
506
- mockChainTracker
507
- )
508
- await expect(engine.submit({
509
- beef: exampleBeef,
510
- topics: ['hello']
511
- })).rejects.toHaveProperty('message', 'This server does not support this topic: hello')
512
- })
513
- it('Verifies the BEEF for the provided transaction', async () => {
514
- const engine = new Engine(
515
- {
516
- Hello: mockTopicManager
517
- },
518
- {},
519
- mockStorageEngine,
520
- mockChainTracker
521
- )
522
- await engine.submit({
523
- beef: exampleBeef,
524
- topics: ['Hello']
525
- })
526
- expect(mockChainTracker.isValidRootForHeight).toHaveBeenCalledWith('bb6f640cc4ee56bf38eb5a1969ac0c16caa2d3d202b22bf3735d10eec0ca6e00', 814435)
527
- })
528
- it('Throws an error if an invalid envelope is provided', async () => {
529
- const engine = new Engine(
530
- {
531
- Hello: mockTopicManager
532
- },
533
- {},
534
- mockStorageEngine,
535
- mockChainTracker
536
- )
537
- mockChainTracker.isValidRootForHeight.mockReturnValueOnce(Promise.resolve(false))
538
- await expect(engine.submit({
539
- beef: exampleBeef,
540
- topics: ['Hello']
541
- })).rejects.toHaveProperty('message', 'Invalid merkle path for transaction 3ecead27a44d013ad1aae40038acbb1883ac9242406808bb4667c15b4f164eac')
542
- })
543
- describe('Broadcast gating (PHASE 2)', () => {
544
- const makeBroadcaster = (): { broadcast: jest.Mock } => ({
545
- broadcast: jest.fn(async () => ({ status: 'success', txid: exampleTXID, message: 'ok' }))
546
- })
547
- const makeEngine = (broadcaster: any): Engine => new Engine(
548
- { Hello: mockTopicManager },
549
- {},
550
- mockStorageEngine,
551
- mockChainTracker,
552
- undefined,
553
- undefined,
554
- undefined,
555
- broadcaster
556
- )
557
-
558
- it('broadcasts when at least one topic admits outputs', async () => {
559
- const broadcaster = makeBroadcaster()
560
- const engine = makeEngine(broadcaster)
561
- await engine.submit({ beef: exampleBeef, topics: ['Hello'] })
562
- expect(broadcaster.broadcast).toHaveBeenCalledTimes(1)
563
- })
564
-
565
- it('never broadcasts a transaction every topic manager rejected', async () => {
566
- mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => ({
567
- outputsToAdmit: [],
568
- coinsToRetain: []
569
- }))
570
- const broadcaster = makeBroadcaster()
571
- const engine = makeEngine(broadcaster)
572
- await engine.submit({ beef: exampleBeef, topics: ['Hello'] })
573
- expect(broadcaster.broadcast).not.toHaveBeenCalled()
574
- })
575
-
576
- it('never broadcasts when topic validation throws, even with tracked coins consumed', async () => {
577
- // The rejected tx spends previously-admitted coins — a throw must
578
- // still gate the broadcast (this is the rejected-transfer shape).
579
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
580
- mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
581
- throw new Error('rule violation')
582
- })
583
- const broadcaster = makeBroadcaster()
584
- const engine = makeEngine(broadcaster)
585
- await engine.submit({ beef: exampleBeef, topics: ['Hello'] })
586
- expect(broadcaster.broadcast).not.toHaveBeenCalled()
587
- })
588
-
589
- it('broadcasts a consume-only transaction that retains nothing (history purge)', async () => {
590
- // e.g. a KVStore remove that also purges history: previously-admitted
591
- // coins are consumed, nothing admitted, nothing retained. That is an
592
- // acceptance, not a rejection (rejection = throw).
593
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
594
- mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => ({
595
- outputsToAdmit: [],
596
- coinsToRetain: []
597
- }))
598
- const broadcaster = makeBroadcaster()
599
- const engine = makeEngine(broadcaster)
600
- await engine.submit({ beef: exampleBeef, topics: ['Hello'] })
601
- expect(broadcaster.broadcast).toHaveBeenCalledTimes(1)
602
- })
603
-
604
- it('still broadcasts a duplicate (previously accepted) transaction', async () => {
605
- mockStorageEngine.doesAppliedTransactionExist = jest.fn(async () => true)
606
- const broadcaster = makeBroadcaster()
607
- const engine = makeEngine(broadcaster)
608
- await engine.submit({ beef: exampleBeef, topics: ['Hello'] })
609
- expect(broadcaster.broadcast).toHaveBeenCalledTimes(1)
610
- })
611
-
612
- it('broadcasts when outputs are consumed but none admitted (coinsToRetain only)', async () => {
613
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
614
- mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => ({
615
- outputsToAdmit: [],
616
- coinsToRetain: [0]
617
- }))
618
- const broadcaster = makeBroadcaster()
619
- const engine = makeEngine(broadcaster)
620
- await engine.submit({ beef: exampleBeef, topics: ['Hello'] })
621
- expect(broadcaster.broadcast).toHaveBeenCalledTimes(1)
622
- })
623
- })
624
-
625
- describe('For each topic being processed', () => {
626
- it('Checks for duplicate transactions', async () => {
627
- const engine = new Engine(
628
- {
629
- Hello: mockTopicManager
630
- },
631
- {},
632
- mockStorageEngine,
633
- mockChainTracker
634
- )
635
- await engine.submit({
636
- beef: exampleBeef,
637
- topics: ['Hello']
638
- })
639
- expect(mockStorageEngine.doesAppliedTransactionExist).toHaveBeenCalledWith({ txid: exampleTXID, topic: 'Hello' })
640
- })
641
- it('Does not process the output if the transaction was already applied to the topic', async () => {
642
- mockStorageEngine.doesAppliedTransactionExist = jest.fn(async () => true)
643
- const engine = new Engine(
644
- {
645
- Hello: mockTopicManager
646
- },
647
- {},
648
- mockStorageEngine,
649
- mockChainTracker
650
- )
651
- await engine.submit({
652
- beef: exampleBeef,
653
- topics: ['Hello']
654
- })
655
- expect(mockStorageEngine.findOutput).not.toHaveBeenCalled()
656
- expect(mockStorageEngine.markUTXOAsSpent).not.toHaveBeenCalled()
657
- expect(mockStorageEngine.insertOutput).not.toHaveBeenCalled()
658
- })
659
- describe('For each input of the transaction', () => {
660
- it('Acquires the appropriate previous topical UTXOs from the storage engine', async () => {
661
- // Mock findUTXO to return a UTXO
662
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
663
- const engine = new Engine(
664
- {
665
- Hello: mockTopicManager
666
- },
667
- {},
668
- mockStorageEngine,
669
- mockChainTracker
670
- )
671
-
672
- // Submit the utxo
673
- await engine.submit({
674
- beef: exampleBeef,
675
- topics: ['Hello']
676
- })
677
- expect(mockStorageEngine.findOutput).toHaveBeenCalled()
678
- })
679
- it('Includes the appropriate previous topical UTXOs when they are returned from the storage engine', async () => {
680
- // Mock findUTXO to return a UTXO
681
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
682
- const engine = new Engine(
683
- {
684
- Hello: mockTopicManager
685
- },
686
- {},
687
- mockStorageEngine,
688
- mockChainTracker
689
- )
690
-
691
- // Submit the utxo
692
- await engine.submit({
693
- beef: exampleBeef,
694
- topics: ['Hello']
695
- })
696
- expect(mockStorageEngine.findOutput).toHaveBeenCalled()
697
- expect(mockStorageEngine.markUTXOAsSpent).toHaveBeenCalledWith(exampleTXID, 0, 'Hello')
698
- })
699
- })
700
- it('Identifies admissible outputs with the appropriate topic manager', async () => {
701
- // Mock findUTXO to return a UTXO
702
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
703
- const engine = new Engine(
704
- {
705
- Hello: mockTopicManager
706
- },
707
- {},
708
- mockStorageEngine,
709
- mockChainTracker
710
- )
711
-
712
- await engine.submit({
713
- beef: exampleBeef,
714
- topics: ['Hello']
715
- })
716
- expect(engine.managers.Hello.identifyAdmissibleOutputs).toHaveBeenCalledWith(
717
- exampleBeef,
718
- [0],
719
- undefined,
720
- 'current-tx'
721
- )
722
- })
723
- describe('When previous UTXOs were retained by the topic manager', () => {
724
- it('Notifies all lookup services about the output being spent (not deleted, see the comment about this in deleteUTXODeep)', async () => {
725
- // Mock findUTXO to return a UTXO
726
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
727
- const engine = new Engine(
728
- {
729
- Hello: mockTopicManager
730
- },
731
- {
732
- Hello: mockLookupService
733
- },
734
- mockStorageEngine,
735
- mockChainTracker
736
- )
737
-
738
- // Submit the utxo
739
- await engine.submit({
740
- beef: exampleBeef,
741
- topics: ['Hello']
742
- })
743
- expect(engine.lookupServices.Hello.outputSpent).toHaveBeenCalledWith({
744
- mode: 'none',
745
- txid: exampleTXID,
746
- outputIndex: 0,
747
- topic: 'Hello'
748
- })
749
- })
750
- })
751
- describe('When previous UTXOs were not retained by the topic manager', () => {
752
- it('Marks the UTXO as stale, deleting all stale UTXOs by calling deleteUTXODeep', async () => {
753
- // Mock findUTXO to return a UTXO
754
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
755
- const engine = new Engine(
756
- {
757
- Hello: mockTopicManager
758
- },
759
- {
760
- Hello: mockLookupService
761
- },
762
- mockStorageEngine,
763
- mockChainTracker
764
- )
765
-
766
- // Submit the utxo
767
- await engine.submit({
768
- beef: exampleBeef,
769
- topics: ['Hello']
770
- })
771
- // Test that previous UTXOs are deleted
772
- expect(mockStorageEngine.deleteOutput).toHaveBeenCalledWith(exampleTXID, 0, 'hello')
773
- expect(mockLookupService.outputNoLongerRetainedInHistory).toHaveBeenCalledWith(exampleTXID, 0, 'hello')
774
- })
775
- it('Notifies all lookup services about the output being spent (the notification about the actual deletion will come from deleteUTXODeep)', async () => {
776
- // Mock findUTXO to return a UTXO
777
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
778
- const engine = new Engine(
779
- {
780
- Hello: mockTopicManager
781
- },
782
- {
783
- Hello: mockLookupService
784
- },
785
- mockStorageEngine,
786
- mockChainTracker
787
- )
788
-
789
- // Submit the utxo
790
- await engine.submit({
791
- beef: exampleBeef,
792
- topics: ['Hello']
793
- })
794
- // Was the lookup service notified of the output deletion?
795
- expect(mockLookupService.outputNoLongerRetainedInHistory).toHaveBeenCalledWith(exampleTXID, 0, 'hello')
796
- })
797
- })
798
- it('Adds admissible UTXOs to the storage engine', async () => {
799
- // Mock findUTXO to return a UTXO
800
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
801
- const engine = new Engine(
802
- {
803
- hello: mockTopicManager
804
- },
805
- {
806
- Hello: mockLookupService
807
- },
808
- mockStorageEngine,
809
- mockChainTracker
810
- )
811
-
812
- // Submit the utxo
813
- await engine.submit({
814
- beef: exampleBeef,
815
- topics: ['hello']
816
- })
817
- // Test the new UTXO was added
818
- expect(mockStorageEngine.insertOutput).toHaveBeenCalledWith(expect.objectContaining({
819
- ...mockOutput,
820
- score: expect.any(Number)
821
- }))
822
- })
823
- it('Notifies lookup services about incoming admissible UTXOs', async () => {
824
- // Mock findUTXO to return a UTXO
825
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
826
- const engine = new Engine(
827
- {
828
- Hello: mockTopicManager
829
- },
830
- {
831
- Hello: mockLookupService
832
- },
833
- mockStorageEngine,
834
- mockChainTracker
835
- )
836
-
837
- // Submit the utxo
838
- await engine.submit({
839
- beef: exampleBeef,
840
- topics: ['Hello']
841
- })
842
- // Test the lookup service was notified of the new UTXO
843
- expect(mockLookupService.outputAdmittedByTopic).toHaveBeenCalledWith({
844
- mode: 'locking-script',
845
- txid: exampleTXID,
846
- satoshis: 26172,
847
- outputIndex: 0,
848
- lockingScript: exampleTX.outputs[0].lockingScript,
849
- topic: 'Hello'
850
- })
851
- })
852
- describe('For each consumed UTXO', () => {
853
- it('Finds the UTXO', async () => {
854
- // Mock findUTXO to return a UTXO
855
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
856
- mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
857
- return {
858
- outputsToAdmit: [0],
859
- coinsToRetain: [0]
860
- }
861
- })
862
- const engine = new Engine(
863
- {
864
- Hello: mockTopicManager
865
- },
866
- {
867
- Hello: mockLookupService
868
- },
869
- mockStorageEngine,
870
- mockChainTracker
871
- )
872
-
873
- // Submit the utxo
874
- await engine.submit({
875
- beef: exampleBeef,
876
- topics: ['Hello']
877
- })
878
-
879
- // Test a storage engine lookup happens for the utxo consumed
880
- expect(mockStorageEngine.findOutput).toHaveBeenCalledWith(examplePreviousTXID, 0, 'Hello')
881
- })
882
- it('Updates the UTXO to reflect that it is now additionally consumed by the newly-created UTXOs', async () => {
883
- // Mock findUTXO to return a UTXO
884
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
885
- mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
886
- return {
887
- outputsToAdmit: [0],
888
- coinsToRetain: [0]
889
- }
890
- })
891
- const engine = new Engine(
892
- {
893
- Hello: mockTopicManager
894
- },
895
- {
896
- Hello: mockLookupService
897
- },
898
- mockStorageEngine,
899
- mockChainTracker
900
- )
901
-
902
- // Submit the utxo
903
- await engine.submit({
904
- beef: exampleBeef,
905
- topics: ['Hello']
906
- })
907
-
908
- // Test the consumedBy data is updated
909
- expect(mockStorageEngine.updateConsumedBy).toHaveBeenCalledWith(examplePreviousTXID, 0, 'Hello', [{
910
- txid: exampleTXID,
911
- outputIndex: 0
912
- }])
913
- })
914
- })
915
- it('Inserts a new applied transaction to avoid de-duplication', async () => {
916
- // Mock findUTXO to return a UTXO
917
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
918
- mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
919
- return {
920
- outputsToAdmit: [0],
921
- coinsToRetain: [0]
922
- }
923
- })
924
- const engine = new Engine(
925
- {
926
- Hello: mockTopicManager
927
- },
928
- {
929
- Hello: mockLookupService
930
- },
931
- mockStorageEngine,
932
- mockChainTracker
933
- )
934
-
935
- // Submit the utxo
936
- await engine.submit({
937
- beef: exampleBeef,
938
- topics: ['Hello']
939
- })
940
-
941
- // Test the tx is inserted
942
- expect(mockStorageEngine.insertAppliedTransaction).toHaveBeenCalledWith(expect.objectContaining({
943
- txid: exampleTXID,
944
- topic: 'Hello'
945
- }))
946
- })
947
- it('Returns a correct set of admitted topics and outputs', async () => {
948
- // Mock findUTXO to return a UTXO
949
- mockStorageEngine.insertOutput = jest.fn()
950
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
951
- mockTopicManager.identifyAdmissibleOutputs = jest.fn(async () => {
952
- return {
953
- outputsToAdmit: [0],
954
- coinsToRetain: [0]
955
- }
956
- })
957
- const engine = new Engine(
958
- {
959
- Hello: mockTopicManager
960
- },
961
- {
962
- Hello: mockLookupService
963
- },
964
- mockStorageEngine,
965
- mockChainTracker
966
- )
967
-
968
- // Submit the utxo
969
- const results = await engine.submit({
970
- beef: exampleBeef,
971
- topics: ['Hello']
972
- })
973
-
974
- // Test the correct outputs are admitted
975
- expect(results).toEqual({
976
- Hello: {
977
- outputsToAdmit: [0],
978
- coinsToRetain: [0],
979
- coinsRemoved: []
980
- }
981
- })
982
- })
983
- })
984
-
985
- describe('lookup', () => {
986
- it('Throws an error if no lookup service has this provider name', async () => {
987
- const engine = new Engine(
988
- {
989
- Hello: mockTopicManager
990
- },
991
- {
992
- Hello: mockLookupService
993
- },
994
- mockStorageEngine,
995
- mockChainTracker
996
- )
997
-
998
- // Perform a lookup request
999
- await expect(engine.lookup({
1000
- service: 'HelloWorld',
1001
- query: { name: 'Bob' }
1002
- })).rejects.toThrow('Lookup service not found for provider: HelloWorld')
1003
- })
1004
- it('Calls the lookup function from the lookup service', async () => {
1005
- // TODO: Make the default storage engine return something...?
1006
- mockLookupService.lookup = jest.fn(async () => [{ txid: exampleTXID, outputIndex: 0 }])
1007
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
1008
- const engine = new Engine(
1009
- {
1010
- Hello: mockTopicManager
1011
- },
1012
- {
1013
- Hello: mockLookupService
1014
- },
1015
- mockStorageEngine,
1016
- mockChainTracker
1017
- )
1018
-
1019
- // Perform a lookup request
1020
- await engine.lookup({
1021
- service: 'Hello',
1022
- query: { name: 'Bob' }
1023
- })
1024
- expect(mockLookupService.lookup).toHaveBeenCalledWith({
1025
- service: 'Hello',
1026
- query: { name: 'Bob' }
1027
- })
1028
- })
1029
- it('Uses batched output loading when the storage engine supports it', async () => {
1030
- mockLookupService.lookup = jest.fn(async () => [{
1031
- txid: 'mockTXID',
1032
- outputIndex: 0,
1033
- history: undefined
1034
- }])
1035
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
1036
- const findOutputsByOutpoints = jest.fn(async () => [mockOutput])
1037
- ;(mockStorageEngine).findOutputsByOutpoints = findOutputsByOutpoints
1038
- const engine = new Engine(
1039
- {
1040
- Hello: mockTopicManager
1041
- },
1042
- {
1043
- Hello: mockLookupService
1044
- },
1045
- mockStorageEngine,
1046
- mockChainTracker
1047
- )
1048
-
1049
- await engine.lookup({
1050
- service: 'Hello',
1051
- query: { name: 'Bob' }
1052
- })
1053
-
1054
- expect(findOutputsByOutpoints).toHaveBeenCalledWith([{ txid: 'mockTXID', outputIndex: 0 }], true)
1055
- expect(mockStorageEngine.findOutput).not.toHaveBeenCalled()
1056
- })
1057
- describe('For each returned result', () => {
1058
- it('Finds the identified UTXO by its txid and vout', async () => {
1059
- // TODO: Make the default storage engine return something...?
1060
- mockLookupService.lookup = jest.fn(async () => [{
1061
- txid: 'mockTXID',
1062
- outputIndex: 0,
1063
- history: undefined
1064
- }])
1065
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
1066
- const engine = new Engine(
1067
- {
1068
- Hello: mockTopicManager
1069
- },
1070
- {
1071
- Hello: mockLookupService
1072
- },
1073
- mockStorageEngine,
1074
- mockChainTracker
1075
- )
1076
-
1077
- // Perform a lookup request
1078
- await engine.lookup({
1079
- service: 'Hello',
1080
- query: { name: 'Bob' }
1081
- })
1082
- expect(mockStorageEngine.findOutput).toHaveBeenCalledWith(
1083
- 'mockTXID', 0, undefined, undefined, true
1084
- )
1085
- })
1086
- it('Calls getUTXOHistory with the correct UTXO and history parameters', async () => {
1087
- mockLookupService.lookup = jest.fn(async () => [{
1088
- txid: 'mockTXID',
1089
- outputIndex: 0,
1090
- history: undefined
1091
- }])
1092
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
1093
- const engine = new Engine(
1094
- {
1095
- Hello: mockTopicManager
1096
- },
1097
- {
1098
- Hello: mockLookupService
1099
- },
1100
- mockStorageEngine,
1101
- mockChainTracker
1102
- )
1103
- engine.getUTXOHistory = jest.fn(async () => {
1104
- return mockOutput
1105
- })
1106
-
1107
- // Perform a lookup request
1108
- await engine.lookup({
1109
- service: 'Hello',
1110
- query: { name: 'Bob' }
1111
- })
1112
- expect(engine.getUTXOHistory).toHaveBeenCalledWith(
1113
- mockOutput,
1114
- undefined,
1115
- 0,
1116
- expect.anything()
1117
- )
1118
- })
1119
- })
1120
- it('Returns the correct set of hydrated results', async () => {
1121
- mockLookupService.lookup = jest.fn(async () => [{
1122
- txid: 'mockTXID',
1123
- outputIndex: 0,
1124
- history: undefined
1125
- }])
1126
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
1127
- const engine = new Engine(
1128
- {
1129
- Hello: mockTopicManager
1130
- },
1131
- {
1132
- Hello: mockLookupService
1133
- },
1134
- mockStorageEngine,
1135
- mockChainTracker
1136
- )
1137
- engine.getUTXOHistory = jest.fn(async () => {
1138
- return mockOutput
1139
- })
1140
-
1141
- // Perform a lookup request
1142
- const results = await engine.lookup({
1143
- service: 'Hello',
1144
- query: { name: 'Bob' }
1145
- })
1146
- expect(results).toEqual({
1147
- outputs: [{
1148
- beef: mockOutput.beef,
1149
- outputIndex: mockOutput.outputIndex
1150
- }],
1151
- type: 'output-list'
1152
- })
1153
- })
1154
- })
1155
- describe('getUTXOHistory', () => {
1156
- it('Returns the given output if there is no history selector', async () => {
1157
- // Already tested above
1158
- return true
1159
- })
1160
- it('Invokes the history selector function with the correct data', async () => {
1161
- const mockedHistorySelector = jest.fn(async (beef, outputIndex, currentDepth) => {
1162
- if (currentDepth !== 2) {
1163
- return true
1164
- }
1165
- return false
1166
- })
1167
- mockLookupService.lookup = jest.fn(async () => [{
1168
- txid: 'mockTXID',
1169
- outputIndex: 0,
1170
- history: mockedHistorySelector
1171
- }])
1172
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
1173
- const engine = new Engine(
1174
- {
1175
- Hello: mockTopicManager
1176
- },
1177
- {
1178
- Hello: mockLookupService
1179
- },
1180
- mockStorageEngine,
1181
- mockChainTracker
1182
- )
1183
-
1184
- // Perform a lookup request
1185
- await engine.lookup({
1186
- service: 'Hello',
1187
- query: { name: 'Bob' }
1188
- })
1189
- expect(mockedHistorySelector).toHaveBeenCalledWith(
1190
- mockOutput.beef,
1191
- mockOutput.outputIndex,
1192
- 0
1193
- )
1194
- expect(mockedHistorySelector).toHaveBeenCalled()
1195
- })
1196
- it('Returns undefined if history should not be traversed', async () => {
1197
- const mockedHistorySelector = jest.fn(async (beef, outputIndex, currentDepth) => {
1198
- return false
1199
- })
1200
- mockLookupService.lookup = jest.fn(async () => [{
1201
- txid: 'mockTXID',
1202
- outputIndex: 0,
1203
- history: mockedHistorySelector
1204
- }])
1205
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
1206
- const engine = new Engine(
1207
- {
1208
- Hello: mockTopicManager
1209
- },
1210
- {
1211
- Hello: mockLookupService
1212
- },
1213
- mockStorageEngine,
1214
- mockChainTracker
1215
- )
1216
-
1217
- // Perform a lookup request
1218
- const results = await engine.lookup({
1219
- service: 'Hello',
1220
- query: { name: 'Bob' }
1221
- })
1222
- expect(mockedHistorySelector).toHaveBeenCalledWith(
1223
- mockOutput.beef,
1224
- mockOutput.outputIndex,
1225
- 0
1226
- )
1227
- expect(results).toEqual({
1228
- outputs: [],
1229
- type: 'output-list'
1230
- })
1231
- })
1232
- it('Returns undefined if the history selector is a number, and less than the current depth', async () => {
1233
- mockLookupService.lookup = jest.fn(async () => [{
1234
- txid: 'mockTXID',
1235
- outputIndex: 0,
1236
- history: -1
1237
- }])
1238
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
1239
- const engine = new Engine(
1240
- {
1241
- Hello: mockTopicManager
1242
- },
1243
- {
1244
- Hello: mockLookupService
1245
- },
1246
- mockStorageEngine,
1247
- mockChainTracker
1248
- )
1249
-
1250
- // Perform a lookup request
1251
- const results = await engine.lookup({
1252
- service: 'Hello',
1253
- query: { name: 'Bob' }
1254
- })
1255
- expect(results).toEqual({
1256
- outputs: [],
1257
- type: 'output-list'
1258
- })
1259
- })
1260
- 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 () => {
1261
- mockLookupService.lookup = jest.fn(async () => [{
1262
- txid: 'mockTXID',
1263
- outputIndex: 0,
1264
- history: 1
1265
- }])
1266
- mockStorageEngine.findOutput = jest.fn(async () => mockOutput)
1267
- const engine = new Engine(
1268
- {
1269
- Hello: mockTopicManager
1270
- },
1271
- {
1272
- Hello: mockLookupService
1273
- },
1274
- mockStorageEngine,
1275
- mockChainTracker
1276
- )
1277
-
1278
- // Perform a lookup request
1279
- const results = await engine.lookup({
1280
- service: 'Hello',
1281
- query: { name: 'Bob' }
1282
- })
1283
-
1284
- expect(results).toEqual({
1285
- outputs: [{
1286
- beef: mockOutput.beef,
1287
- outputIndex: mockOutput.outputIndex
1288
- }],
1289
- type: 'output-list'
1290
- })
1291
- })
1292
- // it('Traversing history, calls findOutput with any output consumed by this UTXO', async () => {
1293
- // mockLookupService.lookup = jest.fn(async () => [{
1294
- // txid: 'mockTXID',
1295
- // outputIndex: 0,
1296
- // history: 1
1297
- // }])
1298
- // mockStorageEngine.findOutput = jest.fn(async () => ({
1299
- // ...mockOutput,
1300
- // outputsConsumed: [{
1301
- // txid: examplePreviousTXID,
1302
- // outputIndex: 0
1303
- // }]
1304
- // }))
1305
- // const engine = new Engine(
1306
- // {
1307
- // Hello: mockTopicManager
1308
- // },
1309
- // {
1310
- // Hello: mockLookupService
1311
- // },
1312
- // mockStorageEngine,
1313
- // mockChainTracker
1314
- // )
1315
-
1316
- // // Perform a lookup request
1317
- // await engine.lookup({
1318
- // service: 'Hello',
1319
- // query: { name: 'Bob' }
1320
- // })
1321
-
1322
- // expect(mockStorageEngine.findOutput).toHaveBeenCalledWith()
1323
- // })
1324
- // it('Returns the correct envelope based on the history traversal process', async () => {
1325
- // // TODO: Come up with some test data for a simple case, but different than the above code.
1326
- // })
1327
- // it('Returns the correct envelope based on the history traversal process for a more complex multi-layer multi-output graph', async () => {
1328
- // // TODO: Come up with some test data to test the history traversal process better
1329
- // })
1330
- })
1331
- // describe('deleteUTXODeep', () => {
1332
- // it('Finds UTXO by ID if no output was provided', async () => {
1333
-
1334
- // })
1335
- // it('Finds UTXO by TXID and VOUT if no ID was provided', async () => {
1336
-
1337
- // })
1338
- // it('Throws an error if no output can be found', async () => {
1339
-
1340
- // })
1341
- // describe('When no more UTXOs are consumed by this UTXO', () => {
1342
- // it('Deletes the UTXO by ID from the storage engine', async () => {
1343
-
1344
- // })
1345
- // it('Notifies all lookup services about the deletion of the UTXO', async () => {
1346
-
1347
- // })
1348
- // })
1349
- // it('For each UTXO that this UTXO consumes, finds the consumed UTXO by its ID and removes reference to this one', async () => {
1350
-
1351
- // })
1352
- })
1353
- })
1354
-
1355
- // beef27c8f_1 is 1 level deep, i.e. proofs on inputs
1356
- const beef27c8f1 = '0100beef01feafcf0c000802020044ef36fc6c79af360b13f2164c6e772e6e5e2083a54d72894f34710fe6a68cf80302792c8c563b17721744d8f101d79e873aae62816d0fd47dd9d90ada219c1b252d010000059e83aae2cfc6a35477566439dd9d58969cc41a134864c671c9b844f2a409a60101004991c129bba819832d6dd315c3a2065bd31b1a60aff8192be335f83731641e690101002f87724a7387df82417902266d2a4fb2fee857451e963a352ce8acbb279aa9eb010100a9e9eb55d20cef840bacbdb26ad8d6e7680ef7653364830b1e770fa140ece6bb010100f0ecd03fff2df21b0034d331cade50152781be9052dcec54eb95c963ef1cdcbf0101006fdb2c01a266e761a32f53b3ad0fabbca31ae234d31d85d9af9bbd624b321c9b0101008108cad77aabf7b0ee371f42afd773cada534244535f7108c7590df2b9d5f8a2020100000006e9847b33c5d666ccee1dccd52235489d8d58bf432be1b8c0cc7c13af80f10703030000006b483045022100fe8a18e043d693246a56c9b32ff553bea89a0bbe389bc55e8244044dcd865de3022065245c30fbbcc6820e4761048ed28c587bf41ece1933dfb85b8fa8c3433d93504121022f2ac22e73a55aa4ff21e905f66959200642ae74716544f834a48af5aa53fb27ffffffff7e8823d7e3e5565fdb9e53c015ad5b63bc6dd6ee1a9be7f7a8e105c903783d45050000006b483045022100e6332a6459350ff2486a39b6d79cdc0b65b6d52cf898fc457e45880c0db01b2a02206e34126bc81e6f03c6de90278788d6b2be3a3f5d7bd85e6442bbea90b8a5e2394121029780a1a98be5ff17a3228feb830e3bc32395ebecc5cf6de08f7a5685faf66c07ffffffff7e8823d7e3e5565fdb9e53c015ad5b63bc6dd6ee1a9be7f7a8e105c903783d45040000006b483045022100dfba71c6c33d2366b2202ea136271fd2f3a26c0a7295f6dd6018fbe12fc48708022052387ca532247bbfffef4b076fd2fbbdebe85b3cf53c73004897cd07be5d1ad6412103f735eb69c9f0b3c90dd2798b709e12a5b3d176a4690bd2a31ec616af9ecd47b5ffffffff7e8823d7e3e5565fdb9e53c015ad5b63bc6dd6ee1a9be7f7a8e105c903783d45020000006b483045022100f941b74c5da1e7f0ce4d1c08745e4d6b8ef56656199505caeff4b74f54890cec02201eaa0d01112497c89ebe4818687c5f3305629a83abd3b0bf5135feee68f57ca0412103fb0ec51f9e6e45b213cb1deff921e3322624cfb6d4a4ba5dc699867cf163b736ffffffff7e8823d7e3e5565fdb9e53c015ad5b63bc6dd6ee1a9be7f7a8e105c903783d45030000006a47304402205a3a2848350fdc19cae73243bea16dfc1e8fbbd5cecde4b5c88a57790505cebb022043db51d04998d52ec394d0e945a76e9922dd63caffb882242bec2ced2a35c7f1412102bae3e2f47dceaed7db2a212e059a00674a0087e3395b92d9f12cdbefdbc84efaffffffffad49b6f41125ffadab03cb2dcc24caf2c897c2cd4aed6e0a3b5c003f2dde757a030000006b483045022100aa7aaec7fc6c5a2f4d5318b7fcbbca0f223c5c198f8fc0e1c8d59aee7155af1202207a4960fb58aa86eba12852f84fa3cdb4c427c7374fe737b3d5bcd9e2a6d76e3b412102b55cfd872e85b3dd424dcceafe01ed5a87dfb599d354ab7fc50d9ec48af77f6effffffff1a400d0300000000001976a914f182138abcb69c7c2339040605aa0a442cfec44288acc8000000000000001976a914e9d2276f468223a6d650672ab9d4c1271fb157e288acacb00000000000001976a914c0ea0208a097df8146933ae15f3ce545d5451e7388ac7a140000000000001976a9142d9ae3291f9393639fe2c38c9a7a948d136a10b488acea070000000000001976a91435d1c396b7ccb467118a41c2d77ebf79fb5fb7eb88ac17050000000000001976a9140dac1e2ddc975ee6d9f393e2ccd4876edb386cdd88ac51060000000000001976a91422878cb2f57a1ebe513815562003128f6e99eda188ac67040000000000001976a91439a752923ba1ef8b126ba7886646d58f54521c6588ac1e040000000000001976a914a939565af52d3fa974a040f42c35365d76b98f6a88acec030000000000001976a914c90823a53e29cafd35a225cc14e3a44eda143a5788ac05040000000000001976a914af6f8d6aaec3bf123efc17c8977bfad1ec1c859388acec030000000000001976a914d4c5e66a062e634be1c6f0eb1f2af0acebecb47988acf1030000000000001976a914a94a3e84bd740095ab39bed110cd3b3922c8595088ace8030000000000001976a914aa2e3c3bf6f3ee79f5af3b94eab2be60a394ce4c88aceb030000000000001976a91459394fa083ff3726571c79624dba3b38f14d924c88ace8030000000000001976a914726eb3ae6211711aa1624f3d767035b0b44abdc488ace8030000000000001976a9145f4680a099a6dc6891af3094aaab96aa66a6259888ace8030000000000001976a914f1a4fbd99e3923b0934abdb8839c337eb2fd50b888ace8030000000000001976a9147ed0ad307f5fe9993f984e7525c0012e3569594588ace8030000000000001976a91407f386bcdc4892cc2d0be3226b721ceaf04750b288ace8030000000000001976a914b51edaaa3c92d0af94109d952c7ddf13aa345f9988ace8030000000000001976a914018f4e02bd74f17a75f5b3965db4633ab514c57588ace8030000000000001976a914cc8d1f7ba5406b81efbc23da0fcccb4d3fe5b40a88ace8030000000000001976a914db1d4b7bd3e62f535dc3f2c8d0a989b000fa736d88ace8030000000000001976a914b8ea69358da7bea9506b15db6fc6597b3849b0a188ace8030000000000001976a914ecd769fdd67e04786e871c50c1a10504861bd79488ac0000000001000100000001792c8c563b17721744d8f101d79e873aae62816d0fd47dd9d90ada219c1b252d070000006b48304502210090368b5925795abb0415a3cd6265d7abae7d3541fb7f2fdb1fdffdd916b3fce3022068136da44df8ee367807c7c59c950ea8d428adfe142268cd6371cf4c1b4c3208412102858ea1804708d1e16e77b49ba1559b3742797f5f43efd3922fa3d21557521d7cffffffff03f401000000000000fde10121026afd108ddbc05e093207ebdf06557a3d66e15b96accbf1766c2e7d6efa3d1981ac22313852713463583334356b6a7461734467764b41455a435135564c5a72516f76786d2081d846bcae65ce281829b6d81eff3862a68b8eaf4e99ad97c0c4ad2e149631c020f936ace2d2b4034710dec819e343f69ddbb1a16d721b88413b697c2475f75f504c4e8470062e6966cb7ec80864e5e1217ecf2b3f113bb5307e1ec1d338678e54b426a11a267e990f7dcd85072c7b3870fabb54812d7f8a7aaa672ce01e4f8a2c6a1d532849877329bd23be0b3383fcb437ffd1ff05aad824343bfe5bae52d5775ed99ba9d5a72038dd50f81153e674a8b1c5d659aac361e660cc5657b291eda149de2505efb0bbac0a353331393633353238334c797f3040f01934bb25733d4700a0309194a7defd8ceea7a8a59ad279c6237e2917264a115773777c5746546288b15ec1cf6788b801dd3952fec804cadbba2ff4e05464735e2db83c3509b59119efc843716c34becd2884f011f2e7f61f382237891875115b6ea5bac1c3e9ca6c575b44547d589c38c1b5a8cadb463044022016e1fdb226b465f3151aaa1e033b122dd58f3b495d2846d532f784ec8f6fc9ae022031dc12155c02e7f588c9f6d975a79c9c26cf341c1c88578cd6a87a3c01f6a73d6d6d6d6dc8000000000000001976a914010b7bfa8ff585b6d95f8a18a998cbd87508bd4188aca9010000000000001976a914254c6f1ce67d27ae4dafb03d2ba08318df2883c588ac0000000000'
1357
- // beef27c8f_0 is 0 levels deep, i.e. tx has merklePath
1358
- const beef27c8f0 = '0100beef01fef4f10c000902fd020100ab2d9b3bbfc2ecf5c834f7719bf10dcabef31cfa3b78fa714bd3a2b3958fc6b5fd0301029515629d935d81e704ca97b8dc02a698c39d78f06bbb3d8d46bcaa5178f3c827018000fc94b1da08b5d0850afcc4948a9e129a2c2c0bb629090ca84fef5d326ceadbc2014100fa78356192a22189dd3c2a03bfb2e043667a55a85386a8d0f0853bbaa54ba748012100e37888006acc37cbfc6a459117c3e957b00e8ff3a90d93bc9aaa3fc9d972453701110052564e444cbe4b1f24015d91f5cfd7eff63e5f403613a7b48e77efa32761f950010900acef65445ef232f1fcb527bab67fccc90ae9d0c610a4f752d84c5e10a3415d74010500e1fadd176551150808ae96ff2b4dff487c432788d23db25fd3fb5e8b3e16b96f0103003e530f146a992ed9378807fa43bc1e6033de0711d728ee3a0ea1507b5ffff2940100006c8eca8a9d680ab2ae4dc1f0e247f282de3f32b99175e6f9ddb4f8e76ae0bf1b010100000001792c8c563b17721744d8f101d79e873aae62816d0fd47dd9d90ada219c1b252d070000006b48304502210090368b5925795abb0415a3cd6265d7abae7d3541fb7f2fdb1fdffdd916b3fce3022068136da44df8ee367807c7c59c950ea8d428adfe142268cd6371cf4c1b4c3208412102858ea1804708d1e16e77b49ba1559b3742797f5f43efd3922fa3d21557521d7cffffffff03f401000000000000fde10121026afd108ddbc05e093207ebdf06557a3d66e15b96accbf1766c2e7d6efa3d1981ac22313852713463583334356b6a7461734467764b41455a435135564c5a72516f76786d2081d846bcae65ce281829b6d81eff3862a68b8eaf4e99ad97c0c4ad2e149631c020f936ace2d2b4034710dec819e343f69ddbb1a16d721b88413b697c2475f75f504c4e8470062e6966cb7ec80864e5e1217ecf2b3f113bb5307e1ec1d338678e54b426a11a267e990f7dcd85072c7b3870fabb54812d7f8a7aaa672ce01e4f8a2c6a1d532849877329bd23be0b3383fcb437ffd1ff05aad824343bfe5bae52d5775ed99ba9d5a72038dd50f81153e674a8b1c5d659aac361e660cc5657b291eda149de2505efb0bbac0a353331393633353238334c797f3040f01934bb25733d4700a0309194a7defd8ceea7a8a59ad279c6237e2917264a115773777c5746546288b15ec1cf6788b801dd3952fec804cadbba2ff4e05464735e2db83c3509b59119efc843716c34becd2884f011f2e7f61f382237891875115b6ea5bac1c3e9ca6c575b44547d589c38c1b5a8cadb463044022016e1fdb226b465f3151aaa1e033b122dd58f3b495d2846d532f784ec8f6fc9ae022031dc12155c02e7f588c9f6d975a79c9c26cf341c1c88578cd6a87a3c01f6a73d6d6d6d6dc8000000000000001976a914010b7bfa8ff585b6d95f8a18a998cbd87508bd4188aca9010000000000001976a914254c6f1ce67d27ae4dafb03d2ba08318df2883c588ac000000000100'
1359
- // full txid
1360
- const txid27c8f = '27c8f37851aabc468d3dbb6bf0789dc398a602dcb897ca04e7815d939d621595'
1361
-
1362
- /*
1363
- txid 17d1829ba8424b97369ee8b528ee8b65d9d4b9c08d037d224a7be7c025f78f78 output 0 1196 sats
1364
- txid 9426201003b01e5bb66e9240a1cc337174238e816a25a06ba532fa67af4457d7 output 0 1197 sats
1365
- txid 509f5ef79c18504fdfe21e67608f56bacb8d8a200634e46f60a9dc8430dcd6e9 output 0 1198 sats
1366
- txid 877734db8eb917d0e2174a4bdddc085b34f67cedd6b94628c4feb1b979a2b2c5 output 0 1199 sats
1367
- txid 37abad7168d47d4e107d0f9f96813a4feef6ea346482fce554c3fade85d45409 output 0 1200 sats
1368
- txid d786db393ec7f1315bee2cbd3aa47634394f57c861feed8c075563308f14c237 output 0 1568 sats
1369
- txid a7d4cac391f5b3d8dedaacb3cd8b5afac54df2d8e043ae0452abcfe190686494 output 17 1132 sats
1370
- */
1371
- const txid17d182 = '17d1829ba8424b97369ee8b528ee8b65d9d4b9c08d037d224a7be7c025f78f78'
1372
- const beef17d1820 = '0100beef01feaff60c000c02fd660302788ff725c0e77b4a227d038dc0b9d4d9658bee28b5e89e36974b42a89b82d117fd670300fe2b9c31ae10a15803f91f63df0cf6a5b6771db9dd831e5bc37a36460a1ffa0701fdb201009bf0e3a5854c4b4c8d62c0ade2754ae1bbc0f16a3e49251ed964486b8dc387f101d80003ae2ec810744139e9989e4f36d8f5ebb160981481e68647890d11e3dda971fa016d0081efcddc14858ead1f427f235d2b21c8de43e33bfeb155ed79f666dd4dd490c2013700a6a847f6e264cfa64c0b6aad6b74912c63c6fc89728be813d104abd2724b98a5011a0062c7d393b9518b05d2d0891ae8c2cd840c93e051d2944bb8e651c976301c4e50010c008d8ee79c1a042cf43f4dcd880aeb4ff7f54552cd3a4c9a40c72cc16ed6847cee01070050194d11d12eddf5e56926b28778f0f64ad54e79eb4df4ce7834ddfc57bf882d010200bd66a0cdbb0af933166931b1050dd108e02d76cff19afd614e681fcbdf1be0740100008b36364159630b47b4c27f6e1e056e820b0aa350ab5dde2520fc1753129b628c010100cab620ac48bb55de40c8c5ddb62bbc44c907fbf5d882326d427f4cd4b34334800101001163b76c2c6824c07a53af2ea86da1b1d946340d9377d3b9b84560e074aea3a6010100000001d75744af67fa32a56ba0256a818e23747133cca140926eb65b1eb00310202694000000006a473044022027137acdeb7583e966a9e8825c9f5edb909b2fd46d40e6ee83846d164529630e0220644e82a46d813fac2836650ce832cdba7eb46b6c134982b36e84f5f08a921dbb412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001ac040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac000000000100'
1373
- const beef17d1824 = '0100beef01fedcee0c000d02f8020954d485defac354e5fc826434eaf6ee4f3a81969f0f7d104e7dd46871adab37f900fa225908f222f8f7f291731a10e93cdf410189ca01adc688edf5577552aa8ff2017d006cfea8ad6ace7ecce9bbbe729eb9dca25f9be064b150d290588c3364867ee112013f00bfbacc8bfdda7f734c160fcfee972dd9182aede73290367aab62b5ce242e5655011e007a0eb94f2bcf98489247a01551a843d9cf1e091522c32f50ce347ba5057b439d010e00beb857ba543d2bd199e68c63a480c73b83660eca27d2ce105355741f27017876010600d5fb1ea8440e41b4ffde093dd53d6ed741c4fe4fe66ff43f6a4eb9af3c6ddebb010200169db3be130a9e8aff1dbff39abee477d3a224cb3e118c39794bb7ba427b707501000083cd0ce2d2734835c45331cd06f38556cf4e07634d599f59692ad9980175a7f201010022f4a629e99ca5838f56f87b050bdefa9b9f621c08745cb27b941d9c6750d99a01010032b90a4148ff984b0efee309a578cb87d278142c014f1a7fd5351b3e3cd4c7da01010082dc3ea570250357fca88652ee1628928136c29eb4910b7e5627e0ffef1d958a010100f5194f41c28960f0dba669f4715229f59f59afb5d55f50a2a7556111b8da67130101009cd7f9a65014cc6fd5c0b71391d0b70eb983ea3b03a2fbd7a13150775855953405010000000237c2148f306355078cedfe61c8574f393476a43abd2cee5b31f1c73e39db86d7000000006a47304402207ee089c64fa810ad88e03f7bad4ae57c9b7de215d222a5bf52f9259d171b717a0220643d3cb4423b5b7ffcb7b2b59eb81dede0a25319aaa0228d27e59d5e238c35a2412102641b5a033ce332a9fc0f81dee7948c099b194758d8352bd5572988ac760ae2faffffffff94646890e1cfab5204ae43e0d8f24dc5fa5a8bcdb3acdaded8b3f591c3cad4a7110000006a47304402205b92eaba4a6c029385e9cf291631b98d0c7aa6851755fb44279e2b1ebd12759702206e96f3bac252f2c5cf5edf7f8828d6358baaa9fe34aa163faeb9b31aac830e47412102577e2a3877a253d9e7587c3d77a4038b4af5e51113393c2713c6d50c9da5de6effffffff03b0040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88acc8000000000000001976a914292e61672fc24a9a70cfa6f73a9af5655652629d88ac13050000000000001976a914ad2ab55d09559074e41e2a2c1c89742e13bee03088ac00000000010001000000010954d485defac354e5fc826434eaf6ee4f3a81969f0f7d104e7dd46871adab37000000006b4830450221009c67deb38aa6b5ed8bfa140e9464e1b1ee2908a296bb70e3d164725254491c6a02207b595dcced1136c276edfc1e5e5901279cf6f0a6432e4c1a908468262e7d9543412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001af040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac00000000000100000001c5b2a279b9b1fec42846b9d6ed7cf6345b08dcdd4b4a17e2d017b98edb347787000000006b483045022100c0992a684be99a07bc87e03bf88701acefc033c1aa57cf4a3b15253e64c8962a022031d5b3af1684a1a1a201eb16fc1fbca8522ae16b9c0d340e8bec48eeea1fe458412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001ae040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac00000000000100000001e9d6dc3084dca9606fe43406208a8dcbba568f60671ee2df4f50189cf75e9f50000000006b483045022100c9c8baa777fe09c1310788614d4eba88b6027d39eada18a5a79c6a33e55919c302205222961b1f63dddd425cd418571797cb3a68aa59ac37331c0fc61fc9de66c34d412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001ad040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac00000000000100000001d75744af67fa32a56ba0256a818e23747133cca140926eb65b1eb00310202694000000006a473044022027137acdeb7583e966a9e8825c9f5edb909b2fd46d40e6ee83846d164529630e0220644e82a46d813fac2836650ce832cdba7eb46b6c134982b36e84f5f08a921dbb412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001ac040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac0000000000'
1374
- const txid942620 = '9426201003b01e5bb66e9240a1cc337174238e816a25a06ba532fa67af4457d7'
1375
- const beef9426200 = '0100beef01fe07ef0c0008022802d75744af67fa32a56ba0256a818e23747133cca140926eb65b1eb00310202694290089598873694014f9706b6074a1b349fcf59df875e58a0cf3a8900c6e5830f33601150036fffb4b882171c594a27d37c5d7827cc706811c3e0acd152e0f3ee1c7ac98f9010b002b3b5c611cbea3bbdf9e99646cb23e80b62fc2dc2e619656157296878d46d8b30104002877b6b97c95fd55c0584d0ccf683c9ad6c60be146dd73605b93ef042da648a0010300d1d01ac14f19d974f8131b9cdb38ae2abcf5403d5c3f0d2772a1c995c136356b01000017ece09932fa7c6d464c633df200003c2328beb26290c7b9b1333eb84c90c544010100cd4147f92f226bc39354a78209db1f521ffd8720e3fcc9c942ee1dd535513d0f0101005daeb3f84528a58700c58c8479f29cd627b07c198602b6c08033340262c0c607010100000001e9d6dc3084dca9606fe43406208a8dcbba568f60671ee2df4f50189cf75e9f50000000006b483045022100c9c8baa777fe09c1310788614d4eba88b6027d39eada18a5a79c6a33e55919c302205222961b1f63dddd425cd418571797cb3a68aa59ac37331c0fc61fc9de66c34d412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001ad040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac000000000100'
1376
- const txid509f5e = '509f5ef79c18504fdfe21e67608f56bacb8d8a200634e46f60a9dc8430dcd6e9'
1377
- const beef509f5e0 = '0100beef01fe05ef0c0009029800fb09f12f5632f7af6409432fc914d1e68fc499270fa29f3b3c43fb00ebeb8ed09902e9d6dc3084dca9606fe43406208a8dcbba568f60671ee2df4f50189cf75e9f50014d00ccb4f6ea2fabdfaf86b0e3837cc702ddde19cb5112e680f00832330b03be1d290127000480365544cd4dca6262cc25567f241d650339773f3f9ef1b9a25b9a034945c30112004ab6bdfecf74af1da1e0574671d06aef63dcde80133d1a6c46beda2ad08abc15010800ce75abf88ee3de3e2f89be0462e1cb0b9470b67947b9880e941bb6565a44bc86010500940d69edc69fc8694485452b961b8df8a2e0c1ff7bdbfeee1e6855542a7e7cd10103002217abcbd7e5431fffc0f52624ee3cf659e39734ae5335d5d196193f7f6e39ab01000082d3a59ca910a1c6fc1f06ab2a5ea80ffee7a39d9a987c85701cfaaf604219f80101008bed85ed818e9d69147000e8687b2efed77e5ee0073e5a2640033715747bd0c2010100000001c5b2a279b9b1fec42846b9d6ed7cf6345b08dcdd4b4a17e2d017b98edb347787000000006b483045022100c0992a684be99a07bc87e03bf88701acefc033c1aa57cf4a3b15253e64c8962a022031d5b3af1684a1a1a201eb16fc1fbca8522ae16b9c0d340e8bec48eeea1fe458412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001ae040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac000000000100'
1378
- const txid877734 = '877734db8eb917d0e2174a4bdddc085b34f67cedd6b94628c4feb1b979a2b2c5'
1379
- const beef8777340 = '0100beef01fedfee0c000e02fd202900eeebd665d8893249c90dabbd50266fb7e0f125dd8ee087a3c8089debf09499d4fd212902c5b2a279b9b1fec42846b9d6ed7cf6345b08dcdd4b4a17e2d017b98edb34778701fd911400b32e903076f6869d4e0a9fa2d00f5c8bad598d3ede5b4bafef88debdb1d3de6901fd490a000a79573c8eb909d30c4e9653f90ea2869e44220d551da81cc96913b2fcd734aa01fd2505003d6db6b39d889be1535a478da0c3428e8107872ac55f0fcfdd2225b6a402d1dc01fd9302008ae31e7e4d4e766814ea11b337b5b51f5d807737f802c44c8d0d75a583a61bde01fd480100e962a171fcc0e79bf55b1433d79564f8d3b2622ecbcb84c9ede792d353e0afb901a5007d0a8d8a3a2f0eb209e82f5e80ef6c4835b7d07c85d6ac8c613cc8754a47bd79015300b070576d92310f1e08a12581ecea4d1d4de48beb15ff7e42eddc1b957fa2909d012800544a58cac6b4dd78cebd84178dc1572693552fec5777a055dad0068bd67aec6101150025674a887fff026640fe9851cbffb37bd8e02780c4f6c0476ed3cee4e4884c2f010b0023aa2b01e8a59c3b79976918d95ae6a044ace2264e3690f9846f452499c0cbad01040078033c2619a3b115bfc71d09addf13e288660be1841cf6769ffb9924aca246970103004d2d6ae3e2c02703b773c49af789b7372b5b71bf92b49e2dc4b921880df984540100003b86537a85e5047596a482ac2dba30d1798b26d200c08a2fb19d8d75b2c2a1b50101000000010954d485defac354e5fc826434eaf6ee4f3a81969f0f7d104e7dd46871adab37000000006b4830450221009c67deb38aa6b5ed8bfa140e9464e1b1ee2908a296bb70e3d164725254491c6a02207b595dcced1136c276edfc1e5e5901279cf6f0a6432e4c1a908468262e7d9543412102b963cd9b5a1419c041a4c4ac46f5205a2b8cb691cd5633e5d498ddcf3d37460b0000000001af040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88ac000000000100'
1380
- const txid37abad = '37abad7168d47d4e107d0f9f96813a4feef6ea346482fce554c3fade85d45409'
1381
- const beef37abad0 = '0100beef01fedcee0c000d02f8020954d485defac354e5fc826434eaf6ee4f3a81969f0f7d104e7dd46871adab37f900fa225908f222f8f7f291731a10e93cdf410189ca01adc688edf5577552aa8ff2017d006cfea8ad6ace7ecce9bbbe729eb9dca25f9be064b150d290588c3364867ee112013f00bfbacc8bfdda7f734c160fcfee972dd9182aede73290367aab62b5ce242e5655011e007a0eb94f2bcf98489247a01551a843d9cf1e091522c32f50ce347ba5057b439d010e00beb857ba543d2bd199e68c63a480c73b83660eca27d2ce105355741f27017876010600d5fb1ea8440e41b4ffde093dd53d6ed741c4fe4fe66ff43f6a4eb9af3c6ddebb010200169db3be130a9e8aff1dbff39abee477d3a224cb3e118c39794bb7ba427b707501000083cd0ce2d2734835c45331cd06f38556cf4e07634d599f59692ad9980175a7f201010022f4a629e99ca5838f56f87b050bdefa9b9f621c08745cb27b941d9c6750d99a01010032b90a4148ff984b0efee309a578cb87d278142c014f1a7fd5351b3e3cd4c7da01010082dc3ea570250357fca88652ee1628928136c29eb4910b7e5627e0ffef1d958a010100f5194f41c28960f0dba669f4715229f59f59afb5d55f50a2a7556111b8da67130101009cd7f9a65014cc6fd5c0b71391d0b70eb983ea3b03a2fbd7a13150775855953401010000000237c2148f306355078cedfe61c8574f393476a43abd2cee5b31f1c73e39db86d7000000006a47304402207ee089c64fa810ad88e03f7bad4ae57c9b7de215d222a5bf52f9259d171b717a0220643d3cb4423b5b7ffcb7b2b59eb81dede0a25319aaa0228d27e59d5e238c35a2412102641b5a033ce332a9fc0f81dee7948c099b194758d8352bd5572988ac760ae2faffffffff94646890e1cfab5204ae43e0d8f24dc5fa5a8bcdb3acdaded8b3f591c3cad4a7110000006a47304402205b92eaba4a6c029385e9cf291631b98d0c7aa6851755fb44279e2b1ebd12759702206e96f3bac252f2c5cf5edf7f8828d6358baaa9fe34aa163faeb9b31aac830e47412102577e2a3877a253d9e7587c3d77a4038b4af5e51113393c2713c6d50c9da5de6effffffff03b0040000000000001976a914ae0545a670c6f739f9a8963aee37be95df0d9b2a88acc8000000000000001976a914292e61672fc24a9a70cfa6f73a9af5655652629d88ac13050000000000001976a914ad2ab55d09559074e41e2a2c1c89742e13bee03088ac000000000100'