@bsv/sdk 2.0.12 → 2.0.13

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 (77) hide show
  1. package/dist/cjs/package.json +1 -1
  2. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js +827 -0
  3. package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  4. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +654 -0
  5. package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  6. package/dist/cjs/src/transaction/MerklePath.js +132 -0
  7. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  8. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  9. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
  10. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  11. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
  12. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  13. package/dist/esm/src/transaction/MerklePath.js +132 -0
  14. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  15. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  16. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
  17. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
  18. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
  19. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
  20. package/dist/types/src/transaction/MerklePath.d.ts +27 -0
  21. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  22. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  23. package/dist/umd/bundle.js +1 -1
  24. package/dist/umd/bundle.js.map +1 -1
  25. package/docs/reference/storage.md +1 -1
  26. package/docs/reference/transaction.md +40 -0
  27. package/package.json +1 -1
  28. package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
  29. package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
  30. package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
  31. package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
  32. package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
  33. package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
  34. package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
  35. package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
  36. package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
  37. package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
  38. package/src/primitives/__tests/Curve.additional.test.ts +208 -0
  39. package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
  40. package/src/primitives/__tests/Hash.additional.test.ts +59 -0
  41. package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
  42. package/src/primitives/__tests/Point.additional.test.ts +503 -0
  43. package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
  44. package/src/primitives/__tests/Random.additional.test.ts +262 -0
  45. package/src/primitives/__tests/Signature.test.ts +333 -0
  46. package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
  47. package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
  48. package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
  49. package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
  50. package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
  51. package/src/script/__tests/Script.additional.test.ts +100 -0
  52. package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
  53. package/src/script/__tests/Spend.additional.test.ts +837 -0
  54. package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
  55. package/src/transaction/MerklePath.ts +155 -0
  56. package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
  57. package/src/transaction/__tests/Broadcaster.test.ts +159 -0
  58. package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
  59. package/src/transaction/__tests/MerklePath.test.ts +80 -0
  60. package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
  61. package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
  62. package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
  63. package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
  64. package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
  65. package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
  66. package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
  67. package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
  68. package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
  69. package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
  70. package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
  71. package/src/wallet/__tests/WERR.test.ts +212 -0
  72. package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
  73. package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
  74. package/src/wallet/__tests/WalletError.test.ts +290 -0
  75. package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
  76. package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
  77. package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
@@ -0,0 +1,349 @@
1
+ import Teranode from '../../../transaction/broadcasters/Teranode'
2
+ import Transaction from '../../../transaction/Transaction'
3
+ import { BinaryFetchClient } from '../../../transaction/http/BinaryFetchClient'
4
+ import { NodejsHttpClient } from '../../../transaction/http/NodejsHttpClient'
5
+ import { RequestOptions } from 'https'
6
+
7
+ // Mock Transaction so tests don't require a fully-formed BSV tx
8
+ jest.mock('../../../transaction/Transaction', () => {
9
+ class MockTransaction {
10
+ toEF (): number[] {
11
+ return [0x01, 0x02, 0x03, 0x04]
12
+ }
13
+
14
+ id (_encoding: string): string {
15
+ return 'mocked_txid'
16
+ }
17
+ }
18
+ return { __esModule: true, default: MockTransaction }
19
+ })
20
+
21
+ describe('Teranode Broadcaster', () => {
22
+ const URL = 'https://teranode.example.com/api/v1/tx'
23
+
24
+ let transaction: Transaction
25
+
26
+ beforeEach(() => {
27
+ transaction = new Transaction()
28
+ })
29
+
30
+ afterEach(() => {
31
+ jest.clearAllMocks()
32
+ if ('window' in globalThis) {
33
+ delete (globalThis as { window?: unknown }).window
34
+ }
35
+ })
36
+
37
+ // ---------------------------------------------------------------------------
38
+ // Constructor
39
+ // ---------------------------------------------------------------------------
40
+
41
+ it('stores the provided URL', () => {
42
+ const mockFetch = jest.fn()
43
+ const broadcaster = new Teranode(URL, new BinaryFetchClient(mockFetch))
44
+ expect(broadcaster.URL).toBe(URL)
45
+ })
46
+
47
+ it('stores the provided httpClient', () => {
48
+ const mockFetch = jest.fn()
49
+ const httpClient = new BinaryFetchClient(mockFetch as any)
50
+ const broadcaster = new Teranode(URL, httpClient)
51
+ expect(broadcaster.httpClient).toBe(httpClient)
52
+ })
53
+
54
+ it('uses binaryHttpClient when no httpClient is provided and window.fetch exists', () => {
55
+ const mockFetch = jest.fn().mockResolvedValue({
56
+ ok: true,
57
+ status: 200,
58
+ statusText: 'OK',
59
+ text: async () => ''
60
+ })
61
+ global.window = { fetch: mockFetch } as unknown as Window & typeof globalThis
62
+
63
+ // Constructor must not throw; httpClient must be defined
64
+ const broadcaster = new Teranode(URL)
65
+ expect(broadcaster.httpClient).toBeDefined()
66
+ expect(typeof broadcaster.httpClient.request).toBe('function')
67
+ })
68
+
69
+ // ---------------------------------------------------------------------------
70
+ // broadcast() — successful response
71
+ // ---------------------------------------------------------------------------
72
+
73
+ it('returns BroadcastResponse on HTTP 200', async () => {
74
+ const mockFetch = mockedFetch({ status: 200, data: '' })
75
+
76
+ const broadcaster = new Teranode(URL, new BinaryFetchClient(mockFetch))
77
+ const response = await broadcaster.broadcast(transaction)
78
+
79
+ expect(response).toEqual({
80
+ status: 'success',
81
+ txid: 'mocked_txid',
82
+ message: 'broadcast successful'
83
+ })
84
+ })
85
+
86
+ it('sends a POST with Content-Type application/octet-stream', async () => {
87
+ const mockFetch = mockedFetch({ status: 200, data: '' })
88
+
89
+ const broadcaster = new Teranode(URL, new BinaryFetchClient(mockFetch))
90
+ await broadcaster.broadcast(transaction)
91
+
92
+ expect(mockFetch).toHaveBeenCalledTimes(1)
93
+ const [calledUrl, calledOptions] = mockFetch.mock.calls[0]
94
+ expect(calledUrl).toBe(URL)
95
+ expect(calledOptions.method).toBe('POST')
96
+ expect(calledOptions.headers['Content-Type']).toBe('application/octet-stream')
97
+ })
98
+
99
+ it('sends a Blob derived from toEF() bytes as request body', async () => {
100
+ const mockFetch = mockedFetch({ status: 200, data: '' })
101
+
102
+ const broadcaster = new Teranode(URL, new BinaryFetchClient(mockFetch))
103
+ await broadcaster.broadcast(transaction)
104
+
105
+ const [, calledOptions] = mockFetch.mock.calls[0]
106
+ expect(calledOptions.body).toBeInstanceOf(Blob)
107
+ })
108
+
109
+ it('returns the tx id from transaction.id("hex")', async () => {
110
+ const mockFetch = mockedFetch({ status: 200, data: '' })
111
+
112
+ const broadcaster = new Teranode(URL, new BinaryFetchClient(mockFetch))
113
+ const response = await broadcaster.broadcast(transaction)
114
+
115
+ expect(response.status).toBe('success')
116
+ if (response.status === 'success') {
117
+ expect(response.txid).toBe('mocked_txid')
118
+ }
119
+ })
120
+
121
+ // ---------------------------------------------------------------------------
122
+ // broadcast() — error response
123
+ // ---------------------------------------------------------------------------
124
+
125
+ it('returns BroadcastFailure with status code string on non-200 response', async () => {
126
+ const mockFetch = mockedFetch({ status: 400, data: 'Bad request data' })
127
+
128
+ const broadcaster = new Teranode(URL, new BinaryFetchClient(mockFetch))
129
+ const response = await broadcaster.broadcast(transaction)
130
+
131
+ expect(response.status).toBe('error')
132
+ if (response.status === 'error') {
133
+ expect(response.code).toBe('400')
134
+ expect(response.description).toBe('Bad request data')
135
+ }
136
+ })
137
+
138
+ it('returns BroadcastFailure on HTTP 500', async () => {
139
+ const mockFetch = mockedFetch({ status: 500, data: 'Internal Server Error body' })
140
+
141
+ const broadcaster = new Teranode(URL, new BinaryFetchClient(mockFetch))
142
+ const response = await broadcaster.broadcast(transaction)
143
+
144
+ expect(response.status).toBe('error')
145
+ if (response.status === 'error') {
146
+ expect(response.code).toBe('500')
147
+ }
148
+ })
149
+
150
+ it('uses "ERR_UNKNOWN" code when response.status is missing/falsy', async () => {
151
+ // Construct a mock where the HttpClient returns a non-ok response with status 0
152
+ const mockHttpClient = {
153
+ request: jest.fn().mockResolvedValue({
154
+ ok: false,
155
+ status: 0,
156
+ statusText: '',
157
+ data: 'some error'
158
+ })
159
+ }
160
+
161
+ const broadcaster = new Teranode(URL, mockHttpClient)
162
+ const response = await broadcaster.broadcast(transaction)
163
+
164
+ expect(response.status).toBe('error')
165
+ if (response.status === 'error') {
166
+ // status.toString() of 0 is "0", not falsy in template — code is "0"
167
+ expect(response.code).toBe('0')
168
+ expect(response.description).toBe('some error')
169
+ }
170
+ })
171
+
172
+ it('uses "Unknown error" description when response.data is null/undefined', async () => {
173
+ const mockHttpClient = {
174
+ request: jest.fn().mockResolvedValue({
175
+ ok: false,
176
+ status: 503,
177
+ statusText: 'Service Unavailable',
178
+ data: null
179
+ })
180
+ }
181
+
182
+ const broadcaster = new Teranode(URL, mockHttpClient)
183
+ const response = await broadcaster.broadcast(transaction)
184
+
185
+ expect(response.status).toBe('error')
186
+ if (response.status === 'error') {
187
+ expect(response.code).toBe('503')
188
+ expect(response.description).toBe('Unknown error')
189
+ }
190
+ })
191
+
192
+ it('uses "Unknown error" description when response.data is undefined', async () => {
193
+ const mockHttpClient = {
194
+ request: jest.fn().mockResolvedValue({
195
+ ok: false,
196
+ status: 422,
197
+ statusText: 'Unprocessable',
198
+ data: undefined
199
+ })
200
+ }
201
+
202
+ const broadcaster = new Teranode(URL, mockHttpClient)
203
+ const response = await broadcaster.broadcast(transaction)
204
+
205
+ expect(response.status).toBe('error')
206
+ if (response.status === 'error') {
207
+ expect(response.description).toBe('Unknown error')
208
+ }
209
+ })
210
+
211
+ // ---------------------------------------------------------------------------
212
+ // broadcast() — network / thrown errors
213
+ // ---------------------------------------------------------------------------
214
+
215
+ it('returns BroadcastFailure with code 500 on network error (Error instance)', async () => {
216
+ const mockFetch = jest.fn().mockRejectedValue(new Error('Network error'))
217
+
218
+ const broadcaster = new Teranode(URL, new BinaryFetchClient(mockFetch))
219
+ const response = await broadcaster.broadcast(transaction)
220
+
221
+ expect(response.status).toBe('error')
222
+ if (response.status === 'error') {
223
+ expect(response.code).toBe('500')
224
+ expect(response.description).toBe('Network error')
225
+ }
226
+ })
227
+
228
+ it('returns "Internal Server Error" description when thrown error has non-string message', async () => {
229
+ const mockHttpClient = {
230
+ request: jest.fn().mockRejectedValue({ message: 42 })
231
+ }
232
+
233
+ const broadcaster = new Teranode(URL, mockHttpClient)
234
+ const response = await broadcaster.broadcast(transaction)
235
+
236
+ expect(response.status).toBe('error')
237
+ if (response.status === 'error') {
238
+ expect(response.code).toBe('500')
239
+ expect(response.description).toBe('Internal Server Error')
240
+ }
241
+ })
242
+
243
+ it('returns "Internal Server Error" when thrown value has no message property', async () => {
244
+ const mockHttpClient = {
245
+ request: jest.fn().mockRejectedValue('plain string error')
246
+ }
247
+
248
+ const broadcaster = new Teranode(URL, mockHttpClient)
249
+ const response = await broadcaster.broadcast(transaction)
250
+
251
+ expect(response.status).toBe('error')
252
+ if (response.status === 'error') {
253
+ expect(response.code).toBe('500')
254
+ expect(response.description).toBe('Internal Server Error')
255
+ }
256
+ })
257
+
258
+ // ---------------------------------------------------------------------------
259
+ // broadcast() — using Node.js https module directly
260
+ // ---------------------------------------------------------------------------
261
+
262
+ it('broadcasts successfully using NodejsHttpClient', async () => {
263
+ const mockHttps = mockedHttps({ status: 200, data: '' })
264
+
265
+ const broadcaster = new Teranode(URL, new NodejsHttpClient(mockHttps))
266
+ const response = await broadcaster.broadcast(transaction)
267
+
268
+ expect(response.status).toBe('success')
269
+ if (response.status === 'success') {
270
+ expect(response.txid).toBe('mocked_txid')
271
+ }
272
+ })
273
+
274
+ it('returns BroadcastFailure using NodejsHttpClient on non-200', async () => {
275
+ const mockHttps = mockedHttps({ status: 503, data: 'Unavailable' })
276
+
277
+ const broadcaster = new Teranode(URL, new NodejsHttpClient(mockHttps))
278
+ const response = await broadcaster.broadcast(transaction)
279
+
280
+ expect(response.status).toBe('error')
281
+ if (response.status === 'error') {
282
+ expect(response.code).toBe('503')
283
+ }
284
+ })
285
+
286
+ // ---------------------------------------------------------------------------
287
+ // Helpers
288
+ // ---------------------------------------------------------------------------
289
+
290
+ function mockedFetch (response: { status: number, data: any }): jest.Mock {
291
+ return jest.fn().mockResolvedValue({
292
+ ok: response.status >= 200 && response.status < 300,
293
+ status: response.status,
294
+ statusText: response.status === 200 ? 'OK' : 'Error',
295
+ // BinaryFetchClient calls res.text() then returns result as data
296
+ text: async () =>
297
+ typeof response.data === 'string'
298
+ ? response.data
299
+ : JSON.stringify(response.data)
300
+ })
301
+ }
302
+
303
+ function mockedHttps (response: { status: number, data: any }): {
304
+ request: (
305
+ url: string,
306
+ options: RequestOptions,
307
+ callback: (res: {
308
+ statusCode: number
309
+ statusMessage: string
310
+ headers: { 'content-type': string }
311
+ on: (event: string, handler: (chunk?: any) => void) => void
312
+ }) => void
313
+ ) => {
314
+ on: jest.Mock
315
+ write: jest.Mock
316
+ end: jest.Mock
317
+ }
318
+ } {
319
+ return {
320
+ request: (
321
+ url: string,
322
+ options: RequestOptions,
323
+ callback: (res: any) => void
324
+ ) => {
325
+ const mockResponse = {
326
+ statusCode: response.status,
327
+ statusMessage: response.status === 200 ? 'OK' : 'Error',
328
+ headers: { 'content-type': 'application/octet-stream' },
329
+ on (event: string, handler: (chunk?: any) => void) {
330
+ if (event === 'data') {
331
+ handler(
332
+ typeof response.data === 'string'
333
+ ? response.data
334
+ : JSON.stringify(response.data)
335
+ )
336
+ }
337
+ if (event === 'end') handler()
338
+ }
339
+ }
340
+ process.nextTick(() => callback(mockResponse))
341
+ return {
342
+ on: jest.fn(),
343
+ write: jest.fn(),
344
+ end: jest.fn()
345
+ }
346
+ }
347
+ }
348
+ }
349
+ })
@@ -0,0 +1,253 @@
1
+ import { BlockHeadersService } from '../../../transaction/chaintrackers/BlockHeadersService'
2
+
3
+ describe('BlockHeadersService', () => {
4
+ const baseUrl = 'https://headers.spv.money'
5
+ const apiKey = 'test-api-key-12345'
6
+ const merkleRoot = 'abc123merkleroot'
7
+ const blockHeight = 800000
8
+
9
+ function makeHttpClient (response: { ok: boolean, status: number, data: any }): { request: jest.Mock } {
10
+ return {
11
+ request: jest.fn().mockResolvedValue(response)
12
+ }
13
+ }
14
+
15
+ describe('constructor', () => {
16
+ it('sets baseUrl from first argument', () => {
17
+ const httpClient = makeHttpClient({ ok: true, status: 200, data: {} })
18
+ const tracker = new BlockHeadersService(baseUrl, { httpClient })
19
+ expect((tracker as any).baseUrl).toBe(baseUrl)
20
+ })
21
+
22
+ it('sets apiKey from config', () => {
23
+ const httpClient = makeHttpClient({ ok: true, status: 200, data: {} })
24
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
25
+ expect((tracker as any).apiKey).toBe(apiKey)
26
+ })
27
+
28
+ it('uses empty string for apiKey when not provided', () => {
29
+ const httpClient = makeHttpClient({ ok: true, status: 200, data: {} })
30
+ const tracker = new BlockHeadersService(baseUrl, { httpClient })
31
+ expect((tracker as any).apiKey).toBe('')
32
+ })
33
+
34
+ it('uses provided httpClient', () => {
35
+ const httpClient = makeHttpClient({ ok: true, status: 200, data: {} })
36
+ const tracker = new BlockHeadersService(baseUrl, { httpClient })
37
+ expect((tracker as any).httpClient).toBe(httpClient)
38
+ })
39
+
40
+ it('falls back to defaultHttpClient when no httpClient provided', () => {
41
+ const tracker = new BlockHeadersService(baseUrl, { apiKey })
42
+ // The httpClient should be set (not undefined) – it comes from defaultHttpClient()
43
+ expect((tracker as any).httpClient).toBeDefined()
44
+ })
45
+ })
46
+
47
+ describe('isValidRootForHeight', () => {
48
+ it('returns true when confirmationState is CONFIRMED', async () => {
49
+ const httpClient = makeHttpClient({
50
+ ok: true,
51
+ status: 200,
52
+ data: {
53
+ confirmationState: 'CONFIRMED',
54
+ confirmations: [
55
+ {
56
+ blockHash: 'hash123',
57
+ blockHeight,
58
+ merkleRoot,
59
+ confirmation: 'CONFIRMED'
60
+ }
61
+ ]
62
+ }
63
+ })
64
+
65
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
66
+ const result = await tracker.isValidRootForHeight(merkleRoot, blockHeight)
67
+ expect(result).toBe(true)
68
+ })
69
+
70
+ it('returns false when confirmationState is UNCONFIRMED', async () => {
71
+ const httpClient = makeHttpClient({
72
+ ok: true,
73
+ status: 200,
74
+ data: {
75
+ confirmationState: 'UNCONFIRMED',
76
+ confirmations: []
77
+ }
78
+ })
79
+
80
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
81
+ const result = await tracker.isValidRootForHeight(merkleRoot, blockHeight)
82
+ expect(result).toBe(false)
83
+ })
84
+
85
+ it('throws on non-ok HTTP response', async () => {
86
+ const httpClient = makeHttpClient({
87
+ ok: false,
88
+ status: 400,
89
+ data: { error: 'Bad Request' }
90
+ })
91
+
92
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
93
+ await expect(tracker.isValidRootForHeight(merkleRoot, blockHeight)).rejects.toThrow(
94
+ `Failed to verify merkleroot for height ${blockHeight} because of an error:`
95
+ )
96
+ })
97
+
98
+ it('throws when httpClient.request rejects', async () => {
99
+ const httpClient = {
100
+ request: jest.fn().mockRejectedValue(new Error('Network failure'))
101
+ }
102
+
103
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
104
+ await expect(tracker.isValidRootForHeight(merkleRoot, blockHeight)).rejects.toThrow(
105
+ 'Failed to verify merkleroot for height'
106
+ )
107
+ })
108
+
109
+ it('sends correct POST body with blockHeight and merkleRoot', async () => {
110
+ const httpClient = makeHttpClient({
111
+ ok: true,
112
+ status: 200,
113
+ data: { confirmationState: 'CONFIRMED', confirmations: [] }
114
+ })
115
+
116
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
117
+ await tracker.isValidRootForHeight(merkleRoot, blockHeight)
118
+
119
+ expect(httpClient.request).toHaveBeenCalledTimes(1)
120
+ const [url, options] = httpClient.request.mock.calls[0]
121
+ expect(url).toBe(`${baseUrl}/api/v1/chain/merkleroot/verify`)
122
+ expect(options.method).toBe('POST')
123
+ expect(options.data).toEqual([
124
+ {
125
+ blockHeight,
126
+ merkleRoot
127
+ }
128
+ ])
129
+ })
130
+
131
+ it('sets Authorization header with Bearer token from apiKey', async () => {
132
+ const httpClient = makeHttpClient({
133
+ ok: true,
134
+ status: 200,
135
+ data: { confirmationState: 'CONFIRMED', confirmations: [] }
136
+ })
137
+
138
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
139
+ await tracker.isValidRootForHeight(merkleRoot, blockHeight)
140
+
141
+ const [, options] = httpClient.request.mock.calls[0]
142
+ expect(options.headers['Authorization']).toBe(`Bearer ${apiKey}`)
143
+ })
144
+
145
+ it('sets Content-Type and Accept headers', async () => {
146
+ const httpClient = makeHttpClient({
147
+ ok: true,
148
+ status: 200,
149
+ data: { confirmationState: 'CONFIRMED', confirmations: [] }
150
+ })
151
+
152
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
153
+ await tracker.isValidRootForHeight(merkleRoot, blockHeight)
154
+
155
+ const [, options] = httpClient.request.mock.calls[0]
156
+ expect(options.headers['Content-Type']).toBe('application/json')
157
+ expect(options.headers['Accept']).toBe('application/json')
158
+ })
159
+ })
160
+
161
+ describe('currentHeight', () => {
162
+ it('returns height from response data', async () => {
163
+ const httpClient = makeHttpClient({
164
+ ok: true,
165
+ status: 200,
166
+ data: { height: 875904 }
167
+ })
168
+
169
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
170
+ const result = await tracker.currentHeight()
171
+ expect(result).toBe(875904)
172
+ })
173
+
174
+ it('sends GET request to correct URL', async () => {
175
+ const httpClient = makeHttpClient({
176
+ ok: true,
177
+ status: 200,
178
+ data: { height: 100 }
179
+ })
180
+
181
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
182
+ await tracker.currentHeight()
183
+
184
+ const [url, options] = httpClient.request.mock.calls[0]
185
+ expect(url).toBe(`${baseUrl}/api/v1/chain/tip/longest`)
186
+ expect(options.method).toBe('GET')
187
+ })
188
+
189
+ it('sets Authorization header with Bearer token for currentHeight', async () => {
190
+ const httpClient = makeHttpClient({
191
+ ok: true,
192
+ status: 200,
193
+ data: { height: 100 }
194
+ })
195
+
196
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
197
+ await tracker.currentHeight()
198
+
199
+ const [, options] = httpClient.request.mock.calls[0]
200
+ expect(options.headers['Authorization']).toBe(`Bearer ${apiKey}`)
201
+ })
202
+
203
+ it('throws on non-ok response', async () => {
204
+ const httpClient = makeHttpClient({
205
+ ok: false,
206
+ status: 500,
207
+ data: { error: 'Internal Server Error' }
208
+ })
209
+
210
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
211
+ await expect(tracker.currentHeight()).rejects.toThrow(
212
+ 'Failed to get current height because of an error:'
213
+ )
214
+ })
215
+
216
+ it('throws when response.data.height is not a number', async () => {
217
+ const httpClient = makeHttpClient({
218
+ ok: true,
219
+ status: 200,
220
+ data: { height: 'not-a-number' }
221
+ })
222
+
223
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
224
+ await expect(tracker.currentHeight()).rejects.toThrow(
225
+ 'Failed to get current height because of an error:'
226
+ )
227
+ })
228
+
229
+ it('throws when response.data is missing height field', async () => {
230
+ const httpClient = makeHttpClient({
231
+ ok: true,
232
+ status: 200,
233
+ data: {}
234
+ })
235
+
236
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
237
+ await expect(tracker.currentHeight()).rejects.toThrow(
238
+ 'Failed to get current height because of an error:'
239
+ )
240
+ })
241
+
242
+ it('throws when httpClient.request rejects', async () => {
243
+ const httpClient = {
244
+ request: jest.fn().mockRejectedValue(new Error('Connection refused'))
245
+ }
246
+
247
+ const tracker = new BlockHeadersService(baseUrl, { apiKey, httpClient })
248
+ await expect(tracker.currentHeight()).rejects.toThrow(
249
+ 'Failed to get current height because of an error: Connection refused'
250
+ )
251
+ })
252
+ })
253
+ })
@@ -0,0 +1,44 @@
1
+ /** eslint-env jest */
2
+ import { defaultChainTracker } from '../DefaultChainTracker'
3
+ import WhatsOnChain from '../WhatsOnChain'
4
+ import ChainTracker from '../../ChainTracker'
5
+
6
+ // --- Tests ------------------------------------------------------------------
7
+
8
+ describe('defaultChainTracker', () => {
9
+ describe('return type and instance', () => {
10
+ it('returns an instance of WhatsOnChain', () => {
11
+ const tracker = defaultChainTracker()
12
+ expect(tracker).toBeInstanceOf(WhatsOnChain)
13
+ })
14
+
15
+ it('satisfies the ChainTracker interface by having isValidRootForHeight method', () => {
16
+ const tracker = defaultChainTracker()
17
+ expect(typeof tracker.isValidRootForHeight).toBe('function')
18
+ })
19
+
20
+ it('returns a new instance on each call', () => {
21
+ const tracker1 = defaultChainTracker()
22
+ const tracker2 = defaultChainTracker()
23
+ expect(tracker1).not.toBe(tracker2)
24
+ })
25
+ })
26
+
27
+ describe('WhatsOnChain defaults', () => {
28
+ it('defaults to mainnet network', () => {
29
+ const tracker = defaultChainTracker() as WhatsOnChain
30
+ expect(tracker.network).toBe('main')
31
+ })
32
+
33
+ it('returns a tracker that responds to isValidRootForHeight as a function', () => {
34
+ const tracker = defaultChainTracker()
35
+ // The method exists and is callable (we do not make real HTTP calls in unit tests)
36
+ expect(tracker.isValidRootForHeight).toBeDefined()
37
+ const returnValue = tracker.isValidRootForHeight('someRoot', 0)
38
+ // It must return a Promise (thenable)
39
+ expect(typeof returnValue.then).toBe('function')
40
+ // Prevent unhandled rejection from real network call
41
+ returnValue.catch(() => {})
42
+ })
43
+ })
44
+ })