@bsv/sdk 1.1.33 → 1.2.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 (214) hide show
  1. package/dist/cjs/mod.js +4 -0
  2. package/dist/cjs/mod.js.map +1 -1
  3. package/dist/cjs/package.json +4 -3
  4. package/dist/cjs/src/auth/Certificate.js +163 -0
  5. package/dist/cjs/src/auth/Certificate.js.map +1 -0
  6. package/dist/cjs/src/auth/index.js +9 -0
  7. package/dist/cjs/src/auth/index.js.map +1 -0
  8. package/dist/cjs/src/compat/BSM.js +17 -7
  9. package/dist/cjs/src/compat/BSM.js.map +1 -1
  10. package/dist/cjs/src/compat/ECIES.js +17 -7
  11. package/dist/cjs/src/compat/ECIES.js.map +1 -1
  12. package/dist/cjs/src/compat/HD.js +17 -7
  13. package/dist/cjs/src/compat/HD.js.map +1 -1
  14. package/dist/cjs/src/compat/Mnemonic.js +17 -7
  15. package/dist/cjs/src/compat/Mnemonic.js.map +1 -1
  16. package/dist/cjs/src/compat/index.js +17 -7
  17. package/dist/cjs/src/compat/index.js.map +1 -1
  18. package/dist/cjs/src/messages/index.js +17 -7
  19. package/dist/cjs/src/messages/index.js.map +1 -1
  20. package/dist/cjs/src/overlay-tools/LookupResolver.js +170 -0
  21. package/dist/cjs/src/overlay-tools/LookupResolver.js.map +1 -0
  22. package/dist/cjs/src/overlay-tools/OverlayAdminTokenTemplate.js +69 -0
  23. package/dist/cjs/src/overlay-tools/OverlayAdminTokenTemplate.js.map +1 -0
  24. package/dist/cjs/src/overlay-tools/SHIPBroadcaster.js +336 -0
  25. package/dist/cjs/src/overlay-tools/SHIPBroadcaster.js.map +1 -0
  26. package/dist/cjs/src/overlay-tools/index.js +29 -0
  27. package/dist/cjs/src/overlay-tools/index.js.map +1 -0
  28. package/dist/cjs/src/primitives/PrivateKey.js +17 -7
  29. package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
  30. package/dist/cjs/src/primitives/TransactionSignature.js +17 -7
  31. package/dist/cjs/src/primitives/TransactionSignature.js.map +1 -1
  32. package/dist/cjs/src/primitives/index.js +17 -7
  33. package/dist/cjs/src/primitives/index.js.map +1 -1
  34. package/dist/cjs/src/script/Spend.js +17 -7
  35. package/dist/cjs/src/script/Spend.js.map +1 -1
  36. package/dist/cjs/src/script/templates/PushDrop.js +218 -0
  37. package/dist/cjs/src/script/templates/PushDrop.js.map +1 -0
  38. package/dist/cjs/src/script/templates/index.js +3 -1
  39. package/dist/cjs/src/script/templates/index.js.map +1 -1
  40. package/dist/cjs/src/transaction/http/DefaultHttpClient.js +1 -1
  41. package/dist/cjs/src/transaction/http/DefaultHttpClient.js.map +1 -1
  42. package/dist/cjs/src/wallet/CachedKeyDeriver.js +177 -0
  43. package/dist/cjs/src/wallet/CachedKeyDeriver.js.map +1 -0
  44. package/dist/cjs/src/wallet/KeyDeriver.js +174 -0
  45. package/dist/cjs/src/wallet/KeyDeriver.js.map +1 -0
  46. package/dist/cjs/src/wallet/ProtoWallet.js +245 -0
  47. package/dist/cjs/src/wallet/ProtoWallet.js.map +1 -0
  48. package/dist/cjs/src/wallet/Wallet.interfaces.js +3 -0
  49. package/dist/cjs/src/wallet/Wallet.interfaces.js.map +1 -0
  50. package/dist/cjs/src/wallet/WalletClient.js +181 -0
  51. package/dist/cjs/src/wallet/WalletClient.js.map +1 -0
  52. package/dist/cjs/src/wallet/WalletError.js +28 -0
  53. package/dist/cjs/src/wallet/WalletError.js.map +1 -0
  54. package/dist/cjs/src/wallet/index.js +34 -0
  55. package/dist/cjs/src/wallet/index.js.map +1 -0
  56. package/dist/cjs/src/wallet/substrates/HTTPWalletWire.js +45 -0
  57. package/dist/cjs/src/wallet/substrates/HTTPWalletWire.js.map +1 -0
  58. package/dist/cjs/src/wallet/substrates/WalletWire.js +3 -0
  59. package/dist/cjs/src/wallet/substrates/WalletWire.js.map +1 -0
  60. package/dist/cjs/src/wallet/substrates/WalletWireCalls.js +36 -0
  61. package/dist/cjs/src/wallet/substrates/WalletWireCalls.js.map +1 -0
  62. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js +1821 -0
  63. package/dist/cjs/src/wallet/substrates/WalletWireProcessor.js.map +1 -0
  64. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js +1305 -0
  65. package/dist/cjs/src/wallet/substrates/WalletWireTransceiver.js.map +1 -0
  66. package/dist/cjs/src/wallet/substrates/XDM.js +130 -0
  67. package/dist/cjs/src/wallet/substrates/XDM.js.map +1 -0
  68. package/dist/cjs/src/wallet/substrates/index.js +33 -0
  69. package/dist/cjs/src/wallet/substrates/index.js.map +1 -0
  70. package/dist/cjs/src/wallet/substrates/window.CWI.js +102 -0
  71. package/dist/cjs/src/wallet/substrates/window.CWI.js.map +1 -0
  72. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  73. package/dist/esm/mod.js +4 -0
  74. package/dist/esm/mod.js.map +1 -1
  75. package/dist/esm/src/auth/Certificate.js +185 -0
  76. package/dist/esm/src/auth/Certificate.js.map +1 -0
  77. package/dist/esm/src/auth/index.js +2 -0
  78. package/dist/esm/src/auth/index.js.map +1 -0
  79. package/dist/esm/src/overlay-tools/LookupResolver.js +167 -0
  80. package/dist/esm/src/overlay-tools/LookupResolver.js.map +1 -0
  81. package/dist/esm/src/overlay-tools/OverlayAdminTokenTemplate.js +64 -0
  82. package/dist/esm/src/overlay-tools/OverlayAdminTokenTemplate.js.map +1 -0
  83. package/dist/esm/src/overlay-tools/SHIPBroadcaster.js +335 -0
  84. package/dist/esm/src/overlay-tools/SHIPBroadcaster.js.map +1 -0
  85. package/dist/esm/src/overlay-tools/index.js +6 -0
  86. package/dist/esm/src/overlay-tools/index.js.map +1 -0
  87. package/dist/esm/src/script/templates/PushDrop.js +215 -0
  88. package/dist/esm/src/script/templates/PushDrop.js.map +1 -0
  89. package/dist/esm/src/script/templates/index.js +1 -0
  90. package/dist/esm/src/script/templates/index.js.map +1 -1
  91. package/dist/esm/src/transaction/http/DefaultHttpClient.js +1 -1
  92. package/dist/esm/src/transaction/http/DefaultHttpClient.js.map +1 -1
  93. package/dist/esm/src/wallet/CachedKeyDeriver.js +174 -0
  94. package/dist/esm/src/wallet/CachedKeyDeriver.js.map +1 -0
  95. package/dist/esm/src/wallet/KeyDeriver.js +172 -0
  96. package/dist/esm/src/wallet/KeyDeriver.js.map +1 -0
  97. package/dist/esm/src/wallet/ProtoWallet.js +207 -0
  98. package/dist/esm/src/wallet/ProtoWallet.js.map +1 -0
  99. package/dist/esm/src/wallet/Wallet.interfaces.js +2 -0
  100. package/dist/esm/src/wallet/Wallet.interfaces.js.map +1 -0
  101. package/dist/esm/src/wallet/WalletClient.js +177 -0
  102. package/dist/esm/src/wallet/WalletClient.js.map +1 -0
  103. package/dist/esm/src/wallet/WalletError.js +25 -0
  104. package/dist/esm/src/wallet/WalletError.js.map +1 -0
  105. package/dist/esm/src/wallet/index.js +9 -0
  106. package/dist/esm/src/wallet/index.js.map +1 -0
  107. package/dist/esm/src/wallet/substrates/HTTPWalletWire.js +42 -0
  108. package/dist/esm/src/wallet/substrates/HTTPWalletWire.js.map +1 -0
  109. package/dist/esm/src/wallet/substrates/WalletWire.js +2 -0
  110. package/dist/esm/src/wallet/substrates/WalletWire.js.map +1 -0
  111. package/dist/esm/src/wallet/substrates/WalletWireCalls.js +34 -0
  112. package/dist/esm/src/wallet/substrates/WalletWireCalls.js.map +1 -0
  113. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js +1816 -0
  114. package/dist/esm/src/wallet/substrates/WalletWireProcessor.js.map +1 -0
  115. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js +1300 -0
  116. package/dist/esm/src/wallet/substrates/WalletWireTransceiver.js.map +1 -0
  117. package/dist/esm/src/wallet/substrates/XDM.js +128 -0
  118. package/dist/esm/src/wallet/substrates/XDM.js.map +1 -0
  119. package/dist/esm/src/wallet/substrates/index.js +8 -0
  120. package/dist/esm/src/wallet/substrates/index.js.map +1 -0
  121. package/dist/esm/src/wallet/substrates/window.CWI.js +100 -0
  122. package/dist/esm/src/wallet/substrates/window.CWI.js.map +1 -0
  123. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  124. package/dist/types/mod.d.ts +4 -0
  125. package/dist/types/mod.d.ts.map +1 -1
  126. package/dist/types/src/auth/Certificate.d.ts +76 -0
  127. package/dist/types/src/auth/Certificate.d.ts.map +1 -0
  128. package/dist/types/src/auth/index.d.ts +2 -0
  129. package/dist/types/src/auth/index.d.ts.map +1 -0
  130. package/dist/types/src/overlay-tools/LookupResolver.d.ts +71 -0
  131. package/dist/types/src/overlay-tools/LookupResolver.d.ts.map +1 -0
  132. package/dist/types/src/overlay-tools/OverlayAdminTokenTemplate.d.ts +44 -0
  133. package/dist/types/src/overlay-tools/OverlayAdminTokenTemplate.d.ts.map +1 -0
  134. package/dist/types/src/overlay-tools/SHIPBroadcaster.d.ts +90 -0
  135. package/dist/types/src/overlay-tools/SHIPBroadcaster.d.ts.map +1 -0
  136. package/dist/types/src/overlay-tools/index.d.ts +6 -0
  137. package/dist/types/src/overlay-tools/index.d.ts.map +1 -0
  138. package/dist/types/src/script/templates/PushDrop.d.ts +53 -0
  139. package/dist/types/src/script/templates/PushDrop.d.ts.map +1 -0
  140. package/dist/types/src/script/templates/index.d.ts +1 -0
  141. package/dist/types/src/script/templates/index.d.ts.map +1 -1
  142. package/dist/types/src/wallet/CachedKeyDeriver.d.ts +92 -0
  143. package/dist/types/src/wallet/CachedKeyDeriver.d.ts.map +1 -0
  144. package/dist/types/src/wallet/KeyDeriver.d.ts +72 -0
  145. package/dist/types/src/wallet/KeyDeriver.d.ts.map +1 -0
  146. package/dist/types/src/wallet/ProtoWallet.d.ts +415 -0
  147. package/dist/types/src/wallet/ProtoWallet.d.ts.map +1 -0
  148. package/dist/types/src/wallet/Wallet.interfaces.d.ts +996 -0
  149. package/dist/types/src/wallet/Wallet.interfaces.d.ts.map +1 -0
  150. package/dist/types/src/wallet/WalletClient.d.ts +182 -0
  151. package/dist/types/src/wallet/WalletClient.d.ts.map +1 -0
  152. package/dist/types/src/wallet/WalletError.d.ts +14 -0
  153. package/dist/types/src/wallet/WalletError.d.ts.map +1 -0
  154. package/dist/types/src/wallet/index.d.ts +9 -0
  155. package/dist/types/src/wallet/index.d.ts.map +1 -0
  156. package/dist/types/src/wallet/substrates/HTTPWalletWire.d.ts +9 -0
  157. package/dist/types/src/wallet/substrates/HTTPWalletWire.d.ts.map +1 -0
  158. package/dist/types/src/wallet/substrates/WalletWire.d.ts +7 -0
  159. package/dist/types/src/wallet/substrates/WalletWire.d.ts.map +1 -0
  160. package/dist/types/src/wallet/substrates/WalletWireCalls.d.ts +33 -0
  161. package/dist/types/src/wallet/substrates/WalletWireCalls.d.ts.map +1 -0
  162. package/dist/types/src/wallet/substrates/WalletWireProcessor.d.ts +18 -0
  163. package/dist/types/src/wallet/substrates/WalletWireProcessor.d.ts.map +1 -0
  164. package/dist/types/src/wallet/substrates/WalletWireTransceiver.d.ts +196 -0
  165. package/dist/types/src/wallet/substrates/WalletWireTransceiver.d.ts.map +1 -0
  166. package/dist/types/src/wallet/substrates/XDM.d.ts +412 -0
  167. package/dist/types/src/wallet/substrates/XDM.d.ts.map +1 -0
  168. package/dist/types/src/wallet/substrates/index.d.ts +8 -0
  169. package/dist/types/src/wallet/substrates/index.d.ts.map +1 -0
  170. package/dist/types/src/wallet/substrates/window.CWI.d.ts +410 -0
  171. package/dist/types/src/wallet/substrates/window.CWI.d.ts.map +1 -0
  172. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  173. package/dist/umd/bundle.js +1 -1
  174. package/docs/overlay-tools.md +537 -0
  175. package/docs/script.md +135 -0
  176. package/docs/totp.md +119 -0
  177. package/docs/wallet-substrates.md +10 -0
  178. package/docs/wallet.md +3718 -0
  179. package/mod.ts +5 -1
  180. package/package.json +44 -3
  181. package/src/auth/Certificate.ts +233 -0
  182. package/src/auth/__tests/Certificate.test.ts +282 -0
  183. package/src/auth/index.ts +1 -0
  184. package/src/overlay-tools/LookupResolver.ts +228 -0
  185. package/src/overlay-tools/OverlayAdminTokenTemplate.ts +79 -0
  186. package/src/overlay-tools/SHIPBroadcaster.ts +405 -0
  187. package/src/overlay-tools/__tests/LookupResolver.test.ts +1403 -0
  188. package/src/overlay-tools/__tests/OverlayAdminTokenTemplate.test.ts +69 -0
  189. package/src/overlay-tools/__tests/SHIPBroadcaster.test.ts +904 -0
  190. package/src/overlay-tools/index.ts +5 -0
  191. package/src/script/templates/PushDrop.ts +246 -0
  192. package/src/script/templates/__tests/PushDrop.test.ts +158 -0
  193. package/src/script/templates/index.ts +1 -0
  194. package/src/transaction/http/DefaultHttpClient.ts +1 -1
  195. package/src/wallet/CachedKeyDeriver.ts +193 -0
  196. package/src/wallet/KeyDeriver.ts +178 -0
  197. package/src/wallet/ProtoWallet.ts +732 -0
  198. package/src/wallet/Wallet.interfaces.ts +1170 -0
  199. package/src/wallet/WalletClient.ts +201 -0
  200. package/src/wallet/WalletError.ts +27 -0
  201. package/src/wallet/__tests/CachedKeyDeriver.test.ts +322 -0
  202. package/src/wallet/__tests/KeyDeriver.test.ts +118 -0
  203. package/src/wallet/__tests/ProtoWallet.test.ts +543 -0
  204. package/src/wallet/index.ts +8 -0
  205. package/src/wallet/substrates/HTTPWalletWire.ts +47 -0
  206. package/src/wallet/substrates/WalletWire.ts +6 -0
  207. package/src/wallet/substrates/WalletWireCalls.ts +34 -0
  208. package/src/wallet/substrates/WalletWireProcessor.ts +2046 -0
  209. package/src/wallet/substrates/WalletWireTransceiver.ts +1454 -0
  210. package/src/wallet/substrates/XDM.ts +157 -0
  211. package/src/wallet/substrates/__tests/WalletWire.integration.test.ts +2194 -0
  212. package/src/wallet/substrates/__tests/XDM.test.ts +659 -0
  213. package/src/wallet/substrates/index.ts +7 -0
  214. package/src/wallet/substrates/window.CWI.ts +133 -0
@@ -0,0 +1,904 @@
1
+ import SHIPCast from '../../../dist/cjs/src/overlay-tools/SHIPBroadcaster.js'
2
+ import LookupResolver from '../../../dist/cjs/src/overlay-tools/LookupResolver.js'
3
+ import { PrivateKey } from '../../../dist/cjs/src/primitives/index.js'
4
+ import { Transaction } from '../../../dist/cjs/src/transaction/index.js'
5
+ import OverlayAdminTokenTemplate from '../../../dist/cjs/src/overlay-tools/OverlayAdminTokenTemplate.js'
6
+ import ProtoWallet from '../../../dist/cjs/src/wallet/ProtoWallet.js'
7
+
8
+ const mockFacilitator = {
9
+ send: jest.fn()
10
+ }
11
+
12
+ const mockResolver = {
13
+ query: jest.fn()
14
+ }
15
+
16
+ describe('SHIPCast', () => {
17
+ beforeEach(() => {
18
+ mockFacilitator.send.mockReset()
19
+ mockResolver.query.mockReset()
20
+ })
21
+
22
+ it('Handles constructor errors', () => {
23
+ expect(() => new SHIPCast([])).toThrow(new Error('At least one topic is required for broadcast.'))
24
+ expect(() => new SHIPCast(['badprefix_foo'])).toThrow(new Error('Every topic must start with "tm_".'))
25
+ })
26
+
27
+ it('should broadcast to a single SHIP host found via resolver', async () => {
28
+ const shipHostKey = new PrivateKey(42)
29
+ const shipWallet = new ProtoWallet(shipHostKey)
30
+ const shipLib = new OverlayAdminTokenTemplate(shipWallet)
31
+ const shipScript = await shipLib.lock('SHIP', 'https://shiphost.com', 'tm_foo')
32
+ const shipTx = new Transaction(1, [], [{
33
+ lockingScript: shipScript,
34
+ satoshis: 1
35
+ }], 0)
36
+
37
+ // Resolver returns one host interested in 'tm_foo' topic
38
+ mockResolver.query.mockReturnValueOnce({
39
+ type: 'output-list',
40
+ outputs: [{
41
+ beef: shipTx.toBEEF(),
42
+ outputIndex: 0
43
+ }]
44
+ })
45
+
46
+ // Host responds successfully
47
+ mockFacilitator.send.mockReturnValueOnce({
48
+ tm_foo: {
49
+
50
+ outputsToAdmit: [0],
51
+ coinsToRetain: []
52
+ }
53
+ })
54
+
55
+ const b = new SHIPCast(['tm_foo'], {
56
+ facilitator: mockFacilitator,
57
+ resolver: mockResolver as unknown as LookupResolver
58
+ })
59
+ const testTx = new Transaction(1, [], [], 0)
60
+ const response = await b.broadcast(testTx)
61
+
62
+ expect(response).toEqual({
63
+ status: 'success',
64
+ txid: testTx.id('hex'),
65
+ message: 'Sent to 1 Overlay Services host.'
66
+ })
67
+
68
+ expect(mockResolver.query).toHaveBeenCalledWith({
69
+ service: 'ls_ship',
70
+ query: {
71
+ topics: ['tm_foo']
72
+ }
73
+ })
74
+
75
+ expect(mockFacilitator.send).toHaveBeenCalledWith('https://shiphost.com', {
76
+ beef: testTx.toBEEF(),
77
+ topics: ['tm_foo']
78
+ })
79
+ })
80
+
81
+ it('should be resilient to malformed or corrupted SHIP data, to the extent possible', async () => {
82
+ const shipHostKey = new PrivateKey(42)
83
+ const shipWallet = new ProtoWallet(shipHostKey)
84
+ const shipLib = new OverlayAdminTokenTemplate(shipWallet)
85
+ // First SHIP is for wrong topic
86
+ const shipScript = await shipLib.lock('SHIP', 'https://shiphost.com', 'tm_wrong')
87
+ const shipTx = new Transaction(1, [], [{
88
+ lockingScript: shipScript,
89
+ satoshis: 1
90
+ }], 0)
91
+ const shipHostKey2 = new PrivateKey(43)
92
+ const shipWallet2 = new ProtoWallet(shipHostKey2)
93
+ const shipLib2 = new OverlayAdminTokenTemplate(shipWallet2)
94
+ // Second SHIP is for correct topic
95
+ const shipScript2 = await shipLib2.lock('SHIP', 'https://shiphost2.com', 'tm_foo')
96
+ const shipTx2 = new Transaction(1, [], [{
97
+ lockingScript: shipScript2,
98
+ satoshis: 1
99
+ }], 0)
100
+
101
+ // Resolver returns two hosts, both the correct and the corrupted ones.
102
+ mockResolver.query.mockReturnValueOnce({
103
+ type: 'output-list',
104
+ outputs: [{
105
+ beef: shipTx.toBEEF(),
106
+ outputIndex: 0
107
+ }, {
108
+ beef: shipTx2.toBEEF(),
109
+ outputIndex: 0
110
+ }]
111
+ })
112
+
113
+ // Host responds successfully
114
+ mockFacilitator.send.mockReturnValue({
115
+ tm_foo: {
116
+ outputsToAdmit: [0],
117
+ coinsToRetain: []
118
+ }
119
+ })
120
+
121
+ const b = new SHIPCast(['tm_foo'], {
122
+ facilitator: mockFacilitator,
123
+ resolver: mockResolver as unknown as LookupResolver
124
+ })
125
+ const testTx = new Transaction(1, [], [], 0)
126
+ let response = await b.broadcast(testTx)
127
+
128
+ expect(response).toEqual({
129
+ status: 'success',
130
+ txid: testTx.id('hex'),
131
+ // One SHIP advertisement should be used, but the second one was invalid
132
+ message: 'Sent to 1 Overlay Services host.'
133
+ })
134
+
135
+ // Transaction should have been sent to the second host, but the first one was invalid
136
+ expect(mockFacilitator.send).toHaveBeenCalledWith('https://shiphost2.com', {
137
+ beef: testTx.toBEEF(),
138
+ topics: ['tm_foo']
139
+ })
140
+ mockFacilitator.send.mockClear()
141
+
142
+ // Resolver returns the wrong type of data
143
+ mockResolver.query.mockReturnValueOnce({
144
+ type: 'invalid',
145
+ bogus: true,
146
+ outputs: {
147
+ different: 'structure'
148
+ }
149
+ })
150
+ await expect(async () => await b.broadcast(testTx)).rejects.toThrow('SHIP answer is not an output list.')
151
+ expect(mockFacilitator.send).not.toHaveBeenCalled()
152
+
153
+ // Resolver returns the wrong output structure
154
+ mockResolver.query.mockReturnValueOnce({
155
+ type: 'output-list',
156
+ outputs: {
157
+ different: 'structure'
158
+ }
159
+ })
160
+ await expect(async () => await b.broadcast(testTx)).rejects.toThrow('answer.outputs is not iterable')
161
+ expect(mockFacilitator.send).not.toHaveBeenCalled()
162
+
163
+ // Resolver returns corrupted BEEF alongside good data
164
+ mockResolver.query.mockReturnValueOnce({
165
+ type: 'output-list',
166
+ outputs: [{
167
+ beef: shipTx.toBEEF(), // Wrong topic
168
+ outputIndex: 0
169
+ }, {
170
+ beef: [0], // corrupted "rotten" BEEF
171
+ outputIndex: 4
172
+ }, {
173
+ beef: shipTx2.toBEEF(),
174
+ outputIndex: 1 // Wrong output index
175
+ }, {
176
+ beef: shipTx2.toBEEF(),
177
+ outputIndex: 0 // correct
178
+ }]
179
+ })
180
+ response = await b.broadcast(testTx)
181
+ expect(response).toEqual({
182
+ status: 'success',
183
+ txid: testTx.id('hex'),
184
+ // One SHIP advertisement should be used, but the second one was invalid
185
+ message: 'Sent to 1 Overlay Services host.'
186
+ })
187
+
188
+ // Transaction should have been sent to the second host, but the first one was invalid
189
+ expect(mockFacilitator.send).toHaveBeenCalledWith('https://shiphost2.com', {
190
+ beef: testTx.toBEEF(),
191
+ topics: ['tm_foo']
192
+ })
193
+ })
194
+
195
+ it('should fail when transaction cannot be serialized to BEEF', async () => {
196
+ const b = new SHIPCast(['tm_foo'], {
197
+ facilitator: mockFacilitator,
198
+ resolver: mockResolver as unknown as LookupResolver
199
+ })
200
+ const testTx = {
201
+ toBEEF: () => {
202
+ throw new Error('Cannot serialize to BEEF')
203
+ }
204
+ } as unknown as Transaction
205
+
206
+ await expect(b.broadcast(testTx)).rejects.toThrow('Transactions sent via SHIP to Overlay Services must be serializable to BEEF format.')
207
+ })
208
+
209
+ it('should fail when no hosts are interested in the topics', async () => {
210
+ // Resolver returns empty output list
211
+ mockResolver.query.mockReturnValueOnce({
212
+ type: 'output-list',
213
+ outputs: []
214
+ })
215
+
216
+ const b = new SHIPCast(['tm_foo'], {
217
+ facilitator: mockFacilitator,
218
+ resolver: mockResolver as unknown as LookupResolver
219
+ })
220
+ const testTx = new Transaction(1, [], [], 0)
221
+
222
+ const result = await b.broadcast(testTx)
223
+
224
+ expect(result).toEqual({
225
+ status: 'error',
226
+ code: 'ERR_NO_HOSTS_INTERESTED',
227
+ description: 'No hosts are interested in receiving this transaction.'
228
+ })
229
+
230
+ expect(mockResolver.query).toHaveBeenCalledWith({
231
+ service: 'ls_ship',
232
+ query: {
233
+ topics: ['tm_foo']
234
+ }
235
+ })
236
+
237
+ expect(mockFacilitator.send).not.toHaveBeenCalled()
238
+ })
239
+
240
+ it('should fail when all hosts reject the transaction', async () => {
241
+ const shipHostKey = new PrivateKey(42)
242
+ const shipWallet = new ProtoWallet(shipHostKey)
243
+ const shipLib = new OverlayAdminTokenTemplate(shipWallet)
244
+ const shipScript = await shipLib.lock('SHIP', 'https://shiphost.com', 'tm_foo')
245
+ const shipTx = new Transaction(1, [], [{
246
+ lockingScript: shipScript,
247
+ satoshis: 1
248
+ }], 0)
249
+
250
+ // Resolver returns one host
251
+ mockResolver.query.mockReturnValueOnce({
252
+ type: 'output-list',
253
+ outputs: [{
254
+ beef: shipTx.toBEEF(),
255
+ outputIndex: 0
256
+ }]
257
+ })
258
+
259
+ // Host fails
260
+ mockFacilitator.send.mockImplementationOnce(() => {
261
+ throw new Error('Host failed')
262
+ })
263
+
264
+ const b = new SHIPCast(['tm_foo'], {
265
+ facilitator: mockFacilitator,
266
+ resolver: mockResolver as unknown as LookupResolver
267
+ })
268
+ const testTx = new Transaction(1, [], [], 0)
269
+
270
+ const result = await b.broadcast(testTx)
271
+
272
+ expect(result).toEqual({
273
+ status: 'error',
274
+ code: 'ERR_ALL_HOSTS_REJECTED',
275
+ description: 'All SHIP hosts have rejected the transaction.'
276
+ })
277
+
278
+ expect(mockFacilitator.send).toHaveBeenCalled()
279
+ })
280
+
281
+ it('should fail when required specific hosts are not among interested hosts', async () => {
282
+ const shipHostKey = new PrivateKey(42)
283
+ const shipWallet = new ProtoWallet(shipHostKey)
284
+ const shipLib = new OverlayAdminTokenTemplate(shipWallet)
285
+ const shipScript = await shipLib.lock('SHIP', 'https://shiphost.com', 'tm_foo')
286
+ const shipTx = new Transaction(1, [], [{
287
+ lockingScript: shipScript,
288
+ satoshis: 1
289
+ }], 0)
290
+
291
+ // Resolver returns one host
292
+ mockResolver.query.mockReturnValueOnce({
293
+ type: 'output-list',
294
+ outputs: [{
295
+ beef: shipTx.toBEEF(),
296
+ outputIndex: 0
297
+ }]
298
+ })
299
+
300
+ // First host acknowledges 'tm_foo', but it's not the right host.
301
+ mockFacilitator.send.mockImplementationOnce(async (host, { beef, topics }) => {
302
+ const steak = {}
303
+ for (const topic of topics) {
304
+ steak[topic] = {
305
+ outputsToAdmit: topic === 'tm_foo' ? [0] : [],
306
+ coinsToRetain: []
307
+ }
308
+ }
309
+ return steak
310
+ })
311
+
312
+ const b = new SHIPCast(['tm_foo'], {
313
+ facilitator: mockFacilitator,
314
+ resolver: mockResolver as unknown as LookupResolver,
315
+ requireAcknowledgmentFromSpecificHostsForTopics: {
316
+ 'https://anotherhost.com': ['tm_foo']
317
+ },
318
+ requireAcknowledgmentFromAllHostsForTopics: [],
319
+ requireAcknowledgmentFromAnyHostForTopics: []
320
+ })
321
+ const testTx = new Transaction(1, [], [], 0)
322
+ const response = await b.broadcast(testTx)
323
+
324
+ expect(response).toEqual({
325
+ status: 'error',
326
+ code: 'ERR_REQUIRE_ACK_FROM_SPECIFIC_HOSTS_FAILED',
327
+ description: 'Specific hosts did not acknowledge the required topics.'
328
+ })
329
+ })
330
+
331
+ it('should succeed when all hosts acknowledge all topics (default behavior)', async () => {
332
+ const shipHostKey1 = new PrivateKey(42)
333
+ const shipWallet1 = new ProtoWallet(shipHostKey1)
334
+ const shipLib1 = new OverlayAdminTokenTemplate(shipWallet1)
335
+ const shipScript1 = await shipLib1.lock('SHIP', 'https://shiphost1.com', 'tm_foo')
336
+ const shipScript1b = await shipLib1.lock('SHIP', 'https://shiphost1.com', 'tm_bar')
337
+ const shipTx1 = new Transaction(1, [], [{
338
+ lockingScript: shipScript1,
339
+ satoshis: 1
340
+ }, {
341
+ lockingScript: shipScript1b,
342
+ satoshis: 1
343
+ }], 0)
344
+
345
+ const shipHostKey2 = new PrivateKey(43)
346
+ const shipWallet2 = new ProtoWallet(shipHostKey2)
347
+ const shipLib2 = new OverlayAdminTokenTemplate(shipWallet2)
348
+ const shipScript2 = await shipLib2.lock('SHIP', 'https://shiphost2.com', 'tm_bar')
349
+ const shipScript2b = await shipLib2.lock('SHIP', 'https://shiphost2.com', 'tm_foo')
350
+ const shipTx2 = new Transaction(1, [], [{
351
+ lockingScript: shipScript2,
352
+ satoshis: 1
353
+ }, {
354
+ lockingScript: shipScript2b,
355
+ satoshis: 1
356
+ }], 0)
357
+
358
+ // Resolver returns two hosts
359
+ mockResolver.query.mockReturnValueOnce({
360
+ type: 'output-list',
361
+ outputs: [
362
+ { beef: shipTx1.toBEEF(), outputIndex: 0 },
363
+ { beef: shipTx1.toBEEF(), outputIndex: 1 },
364
+ { beef: shipTx2.toBEEF(), outputIndex: 0 },
365
+ { beef: shipTx2.toBEEF(), outputIndex: 1 }
366
+ ]
367
+ })
368
+
369
+ // Both hosts acknowledge all topics
370
+ mockFacilitator.send.mockImplementation(async (host, { topics }) => {
371
+ const steak = {}
372
+ for (const topic of topics) {
373
+ steak[topic] = {
374
+ outputsToAdmit: [0],
375
+ coinsToRetain: []
376
+ }
377
+ }
378
+ return steak
379
+ })
380
+
381
+ const b = new SHIPCast(['tm_foo', 'tm_bar'], {
382
+ facilitator: mockFacilitator,
383
+ resolver: mockResolver as unknown as LookupResolver
384
+ })
385
+ const testTx = new Transaction(1, [], [], 0)
386
+ const response = await b.broadcast(testTx)
387
+
388
+ expect(response).toEqual({
389
+ status: 'success',
390
+ txid: testTx.id('hex'),
391
+ message: 'Sent to 2 Overlay Services hosts.'
392
+ })
393
+
394
+ expect(mockResolver.query).toHaveBeenCalledWith({
395
+ service: 'ls_ship',
396
+ query: {
397
+ topics: ['tm_foo', 'tm_bar']
398
+ }
399
+ })
400
+
401
+ expect(mockFacilitator.send).toHaveBeenCalledTimes(2)
402
+ })
403
+
404
+ it('should fail when a host does not acknowledge all topics (default behavior)', async () => {
405
+ const shipHostKey1 = new PrivateKey(42)
406
+ const shipWallet1 = new ProtoWallet(shipHostKey1)
407
+ const shipLib1 = new OverlayAdminTokenTemplate(shipWallet1)
408
+ const shipScript1 = await shipLib1.lock('SHIP', 'https://shiphost1.com', 'tm_foo')
409
+ const shipTx1 = new Transaction(1, [], [{
410
+ lockingScript: shipScript1,
411
+ satoshis: 1
412
+ }], 0)
413
+
414
+ const shipHostKey2 = new PrivateKey(43)
415
+ const shipWallet2 = new ProtoWallet(shipHostKey2)
416
+ const shipLib2 = new OverlayAdminTokenTemplate(shipWallet2)
417
+ const shipScript2 = await shipLib2.lock('SHIP', 'https://shiphost2.com', 'tm_bar')
418
+ const shipTx2 = new Transaction(1, [], [{
419
+ lockingScript: shipScript2,
420
+ satoshis: 1
421
+ }], 0)
422
+
423
+ // Resolver returns two hosts
424
+ mockResolver.query.mockReturnValueOnce({
425
+ type: 'output-list',
426
+ outputs: [
427
+ { beef: shipTx1.toBEEF(), outputIndex: 0 },
428
+ { beef: shipTx2.toBEEF(), outputIndex: 0 }
429
+ ]
430
+ })
431
+
432
+ // First host acknowledges 'tm_foo'
433
+ mockFacilitator.send.mockImplementationOnce(async (host, { beef, topics }) => {
434
+ const steak = {}
435
+ for (const topic of topics) {
436
+ steak[topic] = {
437
+ outputsToAdmit: [0],
438
+ coinsToRetain: []
439
+ }
440
+ }
441
+ return steak
442
+ })
443
+
444
+ // Second host does not acknowledge any topics
445
+ mockFacilitator.send.mockImplementationOnce(async (host, { beef, topics }) => {
446
+ const steak = {}
447
+ for (const topic of topics) {
448
+ steak[topic] = {
449
+ outputsToAdmit: [],
450
+ coinsToRetain: []
451
+ }
452
+ }
453
+ return steak
454
+ })
455
+
456
+ const b = new SHIPCast(['tm_foo', 'tm_bar'], {
457
+ facilitator: mockFacilitator,
458
+ resolver: mockResolver as unknown as LookupResolver
459
+ })
460
+ const testTx = new Transaction(1, [], [], 0)
461
+ const response = await b.broadcast(testTx)
462
+
463
+ expect(response).toEqual({
464
+ status: 'error',
465
+ code: 'ERR_REQUIRE_ACK_FROM_ALL_HOSTS_FAILED',
466
+ description: 'Not all hosts acknowledged the required topics.'
467
+ })
468
+ })
469
+
470
+ it('should succeed when at least one host acknowledges required topics with requireAcknowledgmentFromAnyHostForTopics set to "any"', async () => {
471
+ const shipHostKey1 = new PrivateKey(42)
472
+ const shipWallet1 = new ProtoWallet(shipHostKey1)
473
+ const shipLib1 = new OverlayAdminTokenTemplate(shipWallet1)
474
+ const shipScript1 = await shipLib1.lock('SHIP', 'https://shiphost1.com', 'tm_foo')
475
+ const shipTx1 = new Transaction(1, [], [{
476
+ lockingScript: shipScript1,
477
+ satoshis: 1
478
+ }], 0)
479
+
480
+ const shipHostKey2 = new PrivateKey(43)
481
+ const shipWallet2 = new ProtoWallet(shipHostKey2)
482
+ const shipLib2 = new OverlayAdminTokenTemplate(shipWallet2)
483
+ const shipScript2 = await shipLib2.lock('SHIP', 'https://shiphost2.com', 'tm_bar')
484
+ const shipTx2 = new Transaction(1, [], [{
485
+ lockingScript: shipScript2,
486
+ satoshis: 1
487
+ }], 0)
488
+
489
+ // Resolver returns two hosts
490
+ mockResolver.query.mockReturnValueOnce({
491
+ type: 'output-list',
492
+ outputs: [
493
+ { beef: shipTx1.toBEEF(), outputIndex: 0 },
494
+ { beef: shipTx2.toBEEF(), outputIndex: 0 }
495
+ ]
496
+ })
497
+
498
+ // First host acknowledges no topics
499
+ mockFacilitator.send.mockImplementationOnce(async (host, { beef, topics }) => {
500
+ const steak = {}
501
+ for (const topic of topics) {
502
+ steak[topic] = {
503
+ outputsToAdmit: [],
504
+ coinsToRetain: []
505
+ }
506
+ }
507
+ return steak
508
+ })
509
+
510
+ // Second host acknowledges 'tm_bar'
511
+ mockFacilitator.send.mockImplementationOnce(async (host, { beef, topics }) => {
512
+ const steak = {}
513
+ for (const topic of topics) {
514
+ steak[topic] = {
515
+ outputsToAdmit: topic === 'tm_bar' ? [0] : [],
516
+ coinsToRetain: []
517
+ }
518
+ }
519
+ return steak
520
+ })
521
+
522
+ const b = new SHIPCast(['tm_foo', 'tm_bar'], {
523
+ facilitator: mockFacilitator,
524
+ resolver: mockResolver as unknown as LookupResolver,
525
+ requireAcknowledgmentFromAnyHostForTopics: 'any',
526
+ requireAcknowledgmentFromAllHostsForTopics: []
527
+ })
528
+
529
+ const testTx = new Transaction(1, [], [], 0)
530
+ const response = await b.broadcast(testTx)
531
+
532
+ expect(response).toEqual({
533
+ status: 'success',
534
+ txid: testTx.id('hex'),
535
+ message: 'Sent to 2 Overlay Services hosts.'
536
+ })
537
+ })
538
+
539
+ it('should fail when no hosts acknowledge required topics with requireAcknowledgmentFromAnyHostForTopics set to "any"', async () => {
540
+ const shipHostKey1 = new PrivateKey(42)
541
+ const shipWallet1 = new ProtoWallet(shipHostKey1)
542
+ const shipLib1 = new OverlayAdminTokenTemplate(shipWallet1)
543
+ const shipScript1 = await shipLib1.lock('SHIP', 'https://shiphost1.com', 'tm_foo')
544
+ const shipTx1 = new Transaction(1, [], [{
545
+ lockingScript: shipScript1,
546
+ satoshis: 1
547
+ }], 0)
548
+
549
+ // Resolver returns one host
550
+ mockResolver.query.mockReturnValueOnce({
551
+ type: 'output-list',
552
+ outputs: [{ beef: shipTx1.toBEEF(), outputIndex: 0 }]
553
+ })
554
+
555
+ // Host acknowledges no topics
556
+ mockFacilitator.send.mockImplementationOnce(async (host, { beef, topics }) => {
557
+ const steak = {}
558
+ for (const topic of topics) {
559
+ steak[topic] = {
560
+ outputsToAdmit: [],
561
+ coinsToRetain: []
562
+ }
563
+ }
564
+ return steak
565
+ })
566
+
567
+ const b = new SHIPCast(['tm_foo'], {
568
+ facilitator: mockFacilitator,
569
+ resolver: mockResolver as unknown as LookupResolver,
570
+ requireAcknowledgmentFromAnyHostForTopics: 'any',
571
+ requireAcknowledgmentFromAllHostsForTopics: []
572
+ })
573
+
574
+ const testTx = new Transaction(1, [], [], 0)
575
+ const response = await b.broadcast(testTx)
576
+
577
+ expect(response).toEqual({
578
+ status: 'error',
579
+ code: 'ERR_REQUIRE_ACK_FROM_ANY_HOST_FAILED',
580
+ description: 'No host acknowledged the required topics.'
581
+ })
582
+ })
583
+
584
+ it('should succeed when specific hosts acknowledge required topics', async () => {
585
+ const shipHostKey1 = new PrivateKey(42)
586
+ const shipWallet1 = new ProtoWallet(shipHostKey1)
587
+ const shipLib1 = new OverlayAdminTokenTemplate(shipWallet1)
588
+ const shipScript1 = await shipLib1.lock('SHIP', 'https://shiphost1.com', 'tm_foo')
589
+ const shipTx1 = new Transaction(1, [], [{
590
+ lockingScript: shipScript1,
591
+ satoshis: 1
592
+ }], 0)
593
+
594
+ const shipHostKey2 = new PrivateKey(43)
595
+ const shipWallet2 = new ProtoWallet(shipHostKey2)
596
+ const shipLib2 = new OverlayAdminTokenTemplate(shipWallet2)
597
+ const shipScript2 = await shipLib2.lock('SHIP', 'https://shiphost2.com', 'tm_bar')
598
+ const shipTx2 = new Transaction(1, [], [{
599
+ lockingScript: shipScript2,
600
+ satoshis: 1
601
+ }], 0)
602
+
603
+ // Resolver returns two hosts
604
+ mockResolver.query.mockReturnValueOnce({
605
+ type: 'output-list',
606
+ outputs: [
607
+ { beef: shipTx1.toBEEF(), outputIndex: 0 },
608
+ { beef: shipTx2.toBEEF(), outputIndex: 0 }
609
+ ]
610
+ })
611
+
612
+ // First host acknowledges 'tm_foo'
613
+ mockFacilitator.send.mockImplementationOnce(async (host, { beef, topics }) => {
614
+ const steak = {}
615
+ for (const topic of topics) {
616
+ steak[topic] = {
617
+ outputsToAdmit: topic === 'tm_foo' ? [0] : [],
618
+ coinsToRetain: []
619
+ }
620
+ }
621
+ return steak
622
+ })
623
+
624
+ // Second host does not acknowledge 'tm_bar'
625
+ mockFacilitator.send.mockImplementationOnce(async (host, { beef, topics }) => {
626
+ const steak = {}
627
+ for (const topic of topics) {
628
+ steak[topic] = {
629
+ outputsToAdmit: [],
630
+ coinsToRetain: []
631
+ }
632
+ }
633
+ return steak
634
+ })
635
+
636
+ const b = new SHIPCast(['tm_foo', 'tm_bar'], {
637
+ facilitator: mockFacilitator,
638
+ resolver: mockResolver as unknown as LookupResolver,
639
+ requireAcknowledgmentFromSpecificHostsForTopics: {
640
+ 'https://shiphost1.com': ['tm_foo']
641
+ },
642
+ requireAcknowledgmentFromAllHostsForTopics: [],
643
+ requireAcknowledgmentFromAnyHostForTopics: []
644
+ })
645
+ const testTx = new Transaction(1, [], [], 0)
646
+ const response = await b.broadcast(testTx)
647
+
648
+ expect(response).toEqual({
649
+ status: 'success',
650
+ txid: testTx.id('hex'),
651
+ message: 'Sent to 2 Overlay Services hosts.'
652
+ })
653
+ })
654
+
655
+ it('should fail when specific hosts do not acknowledge required topics', async () => {
656
+ const shipHostKey1 = new PrivateKey(42)
657
+ const shipWallet1 = new ProtoWallet(shipHostKey1)
658
+ const shipLib1 = new OverlayAdminTokenTemplate(shipWallet1)
659
+ const shipScript1 = await shipLib1.lock('SHIP', 'https://shiphost1.com', 'tm_foo')
660
+ const shipTx1 = new Transaction(1, [], [{
661
+ lockingScript: shipScript1,
662
+ satoshis: 1
663
+ }], 0)
664
+
665
+ // Resolver returns one host
666
+ mockResolver.query.mockReturnValueOnce({
667
+ type: 'output-list',
668
+ outputs: [{ beef: shipTx1.toBEEF(), outputIndex: 0 }]
669
+ })
670
+
671
+ // Host does not acknowledge 'tm_foo'
672
+ mockFacilitator.send.mockImplementationOnce(async (host, { beef, topics }) => {
673
+ const steak = {}
674
+ for (const topic of topics) {
675
+ steak[topic] = {
676
+ outputsToAdmit: [],
677
+ coinsToRetain: []
678
+ }
679
+ }
680
+ return steak
681
+ })
682
+
683
+ const b = new SHIPCast(['tm_foo'], {
684
+ facilitator: mockFacilitator,
685
+ resolver: mockResolver as unknown as LookupResolver,
686
+ requireAcknowledgmentFromSpecificHostsForTopics: {
687
+ 'https://shiphost1.com': ['tm_foo']
688
+ },
689
+ requireAcknowledgmentFromAllHostsForTopics: [],
690
+ requireAcknowledgmentFromAnyHostForTopics: []
691
+ })
692
+
693
+ const testTx = new Transaction(1, [], [], 0)
694
+ const response = await b.broadcast(testTx)
695
+
696
+ expect(response).toEqual({
697
+ status: 'error',
698
+ code: 'ERR_REQUIRE_ACK_FROM_SPECIFIC_HOSTS_FAILED',
699
+ description: 'Specific hosts did not acknowledge the required topics.'
700
+ })
701
+ })
702
+
703
+ it('should handle invalid acknowledgments from hosts gracefully', async () => {
704
+ const shipHostKey = new PrivateKey(42)
705
+ const shipWallet = new ProtoWallet(shipHostKey)
706
+ const shipLib = new OverlayAdminTokenTemplate(shipWallet)
707
+ const shipScript = await shipLib.lock('SHIP', 'https://shiphost.com', 'tm_foo')
708
+ const shipTx = new Transaction(1, [], [{
709
+ lockingScript: shipScript,
710
+ satoshis: 1
711
+ }], 0)
712
+
713
+ // Resolver returns one host
714
+ mockResolver.query.mockReturnValueOnce({
715
+ type: 'output-list',
716
+ outputs: [{
717
+ beef: shipTx.toBEEF(),
718
+ outputIndex: 0
719
+ }]
720
+ })
721
+
722
+ // Host returns invalid acknowledgment
723
+ mockFacilitator.send.mockReturnValueOnce(null)
724
+ const b = new SHIPCast(['tm_foo'], {
725
+ facilitator: mockFacilitator,
726
+ resolver: mockResolver as unknown as LookupResolver
727
+ })
728
+ const testTx = new Transaction(1, [], [], 0)
729
+ const response = await b.broadcast(testTx)
730
+
731
+ // Since the host responded (successfully in terms of HTTP), but with invalid data, we should consider it a failure
732
+ expect(response).toEqual({
733
+ status: 'error',
734
+ code: 'ERR_ALL_HOSTS_REJECTED',
735
+ description: 'All SHIP hosts have rejected the transaction.'
736
+ })
737
+ })
738
+ describe('SHIPCast private methods', () => {
739
+ let shipCast: SHIPCast
740
+
741
+ beforeEach(() => {
742
+ shipCast = new SHIPCast(['tm_foo', 'tm_bar'], {
743
+ facilitator: mockFacilitator,
744
+ resolver: mockResolver as unknown as LookupResolver
745
+ })
746
+ })
747
+
748
+ describe('checkAcknowledgmentFromAllHosts', () => {
749
+ it('should return true when all hosts acknowledge all required topics', () => {
750
+ const hostAcknowledgments = {
751
+ 'https://host1.com': new Set(['tm_foo', 'tm_bar']),
752
+ 'https://host2.com': new Set(['tm_foo', 'tm_bar'])
753
+ }
754
+ const result = (shipCast as any).checkAcknowledgmentFromAllHosts(hostAcknowledgments, ['tm_foo', 'tm_bar'], 'all')
755
+ expect(result).toBe(true)
756
+ })
757
+
758
+ it('should return false when any host does not acknowledge all required topics', () => {
759
+ const hostAcknowledgments = {
760
+ 'https://host1.com': new Set(['tm_foo']),
761
+ 'https://host2.com': new Set(['tm_foo', 'tm_bar'])
762
+ }
763
+ const result = (shipCast as any).checkAcknowledgmentFromAllHosts(hostAcknowledgments, ['tm_foo', 'tm_bar'], 'all')
764
+ expect(result).toBe(false)
765
+ })
766
+
767
+ it('should return true when all hosts acknowledge any of the required topics', () => {
768
+ const hostAcknowledgments = {
769
+ 'https://host1.com': new Set(['tm_foo']),
770
+ 'https://host2.com': new Set(['tm_bar'])
771
+ }
772
+ const result = (shipCast as any).checkAcknowledgmentFromAllHosts(hostAcknowledgments, ['tm_foo', 'tm_bar'], 'any')
773
+ expect(result).toBe(true)
774
+ })
775
+
776
+ it('should return false when any host does not acknowledge any of the required topics', () => {
777
+ const hostAcknowledgments = {
778
+ 'https://host1.com': new Set(),
779
+ 'https://host2.com': new Set(['tm_bar'])
780
+ }
781
+ const result = (shipCast as any).checkAcknowledgmentFromAllHosts(hostAcknowledgments, ['tm_foo', 'tm_bar'], 'any')
782
+ expect(result).toBe(false)
783
+ })
784
+ })
785
+
786
+ describe('checkAcknowledgmentFromAnyHost', () => {
787
+ it('should return true when at least one host acknowledges all required topics', () => {
788
+ const hostAcknowledgments = {
789
+ 'https://host1.com': new Set(['tm_foo', 'tm_bar']),
790
+ 'https://host2.com': new Set(['tm_foo'])
791
+ }
792
+ const result = (shipCast as any).checkAcknowledgmentFromAnyHost(hostAcknowledgments, ['tm_foo', 'tm_bar'], 'all')
793
+ expect(result).toBe(true)
794
+ })
795
+
796
+ it('should return false when no host acknowledges all required topics', () => {
797
+ const hostAcknowledgments = {
798
+ 'https://host1.com': new Set(['tm_foo']),
799
+ 'https://host2.com': new Set(['tm_bar'])
800
+ }
801
+ const result = (shipCast as any).checkAcknowledgmentFromAnyHost(hostAcknowledgments, ['tm_foo', 'tm_bar'], 'all')
802
+ expect(result).toBe(false)
803
+ })
804
+
805
+ it('should return true when at least one host acknowledges any of the required topics', () => {
806
+ const hostAcknowledgments = {
807
+ 'https://host1.com': new Set(['tm_foo']),
808
+ 'https://host2.com': new Set()
809
+ }
810
+ const result = (shipCast as any).checkAcknowledgmentFromAnyHost(hostAcknowledgments, ['tm_foo', 'tm_bar'], 'any')
811
+ expect(result).toBe(true)
812
+ })
813
+
814
+ it('should return false when no host acknowledges any of the required topics', () => {
815
+ const hostAcknowledgments = {
816
+ 'https://host1.com': new Set(),
817
+ 'https://host2.com': new Set()
818
+ }
819
+ const result = (shipCast as any).checkAcknowledgmentFromAnyHost(hostAcknowledgments, ['tm_foo', 'tm_bar'], 'any')
820
+ expect(result).toBe(false)
821
+ })
822
+ })
823
+
824
+ describe('checkAcknowledgmentFromSpecificHosts', () => {
825
+ it('should return true when specific hosts acknowledge all required topics', () => {
826
+ const hostAcknowledgments = {
827
+ 'https://host1.com': new Set(['tm_foo', 'tm_bar']),
828
+ 'https://host2.com': new Set(['tm_foo'])
829
+ }
830
+ const requirements = {
831
+ 'https://host1.com': ['tm_foo', 'tm_bar']
832
+ }
833
+ const result = (shipCast as any).checkAcknowledgmentFromSpecificHosts(hostAcknowledgments, requirements)
834
+ expect(result).toBe(true)
835
+ })
836
+
837
+ it('should return false when specific hosts do not acknowledge all required topics', () => {
838
+ const hostAcknowledgments = {
839
+ 'https://host1.com': new Set(['tm_foo']),
840
+ 'https://host2.com': new Set(['tm_bar'])
841
+ }
842
+ const requirements = {
843
+ 'https://host1.com': ['tm_foo', 'tm_bar']
844
+ }
845
+ const result = (shipCast as any).checkAcknowledgmentFromSpecificHosts(hostAcknowledgments, requirements)
846
+ expect(result).toBe(false)
847
+ })
848
+
849
+ it('should return true when specific hosts acknowledge any of the required topics', () => {
850
+ const hostAcknowledgments = {
851
+ 'https://host1.com': new Set(['tm_foo']),
852
+ 'https://host2.com': new Set(['tm_bar'])
853
+ }
854
+ const requirements = {
855
+ 'https://host1.com': 'any'
856
+ }
857
+ const result = (shipCast as any).checkAcknowledgmentFromSpecificHosts(hostAcknowledgments, requirements)
858
+ expect(result).toBe(true)
859
+ })
860
+
861
+ it('should return false when specific hosts do not acknowledge any of the required topics', () => {
862
+ const hostAcknowledgments = {
863
+ 'https://host1.com': new Set(),
864
+ 'https://host2.com': new Set(['tm_bar'])
865
+ }
866
+ const requirements = {
867
+ 'https://host1.com': 'any'
868
+ }
869
+ const result = (shipCast as any).checkAcknowledgmentFromSpecificHosts(hostAcknowledgments, requirements)
870
+ expect(result).toBe(false)
871
+ })
872
+
873
+ it('should handle multiple hosts with different requirements', () => {
874
+ const hostAcknowledgments = {
875
+ 'https://host1.com': new Set(['tm_foo']),
876
+ 'https://host2.com': new Set(['tm_bar']),
877
+ 'https://host3.com': new Set(['tm_foo', 'tm_bar'])
878
+ }
879
+ const requirements = {
880
+ 'https://host1.com': ['tm_foo'],
881
+ 'https://host2.com': 'any',
882
+ 'https://host3.com': 'all'
883
+ }
884
+ const result = (shipCast as any).checkAcknowledgmentFromSpecificHosts(hostAcknowledgments, requirements)
885
+ expect(result).toBe(true)
886
+ })
887
+
888
+ it('should return false if any specific host fails to meet its requirement', () => {
889
+ const hostAcknowledgments = {
890
+ 'https://host1.com': new Set(['tm_foo']),
891
+ 'https://host2.com': new Set(),
892
+ 'https://host3.com': new Set(['tm_foo'])
893
+ }
894
+ const requirements = {
895
+ 'https://host1.com': ['tm_foo'],
896
+ 'https://host2.com': 'any',
897
+ 'https://host3.com': ['tm_foo', 'tm_bar']
898
+ }
899
+ const result = (shipCast as any).checkAcknowledgmentFromSpecificHosts(hostAcknowledgments, requirements)
900
+ expect(result).toBe(false)
901
+ })
902
+ })
903
+ })
904
+ })