@bsv/sdk 2.0.11 → 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 (106) 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/overlay-tools/HostReputationTracker.js +21 -13
  7. package/dist/cjs/src/overlay-tools/HostReputationTracker.js.map +1 -1
  8. package/dist/cjs/src/primitives/PrivateKey.js +3 -3
  9. package/dist/cjs/src/primitives/PrivateKey.js.map +1 -1
  10. package/dist/cjs/src/script/Spend.js +17 -9
  11. package/dist/cjs/src/script/Spend.js.map +1 -1
  12. package/dist/cjs/src/storage/StorageDownloader.js +6 -6
  13. package/dist/cjs/src/storage/StorageDownloader.js.map +1 -1
  14. package/dist/cjs/src/storage/StorageUtils.js +1 -1
  15. package/dist/cjs/src/storage/StorageUtils.js.map +1 -1
  16. package/dist/cjs/src/transaction/MerklePath.js +168 -27
  17. package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
  18. package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
  19. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
  20. package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
  21. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
  22. package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
  23. package/dist/esm/src/overlay-tools/HostReputationTracker.js +21 -13
  24. package/dist/esm/src/overlay-tools/HostReputationTracker.js.map +1 -1
  25. package/dist/esm/src/primitives/PrivateKey.js +3 -3
  26. package/dist/esm/src/primitives/PrivateKey.js.map +1 -1
  27. package/dist/esm/src/script/Spend.js +17 -9
  28. package/dist/esm/src/script/Spend.js.map +1 -1
  29. package/dist/esm/src/storage/StorageDownloader.js +6 -6
  30. package/dist/esm/src/storage/StorageDownloader.js.map +1 -1
  31. package/dist/esm/src/storage/StorageUtils.js +1 -1
  32. package/dist/esm/src/storage/StorageUtils.js.map +1 -1
  33. package/dist/esm/src/transaction/MerklePath.js +168 -27
  34. package/dist/esm/src/transaction/MerklePath.js.map +1 -1
  35. package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
  36. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
  37. package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
  38. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
  39. package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
  40. package/dist/types/src/overlay-tools/HostReputationTracker.d.ts.map +1 -1
  41. package/dist/types/src/script/Spend.d.ts.map +1 -1
  42. package/dist/types/src/transaction/MerklePath.d.ts +27 -0
  43. package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
  44. package/dist/types/tsconfig.types.tsbuildinfo +1 -1
  45. package/dist/umd/bundle.js +3 -3
  46. package/dist/umd/bundle.js.map +1 -1
  47. package/docs/reference/storage.md +1 -1
  48. package/docs/reference/transaction.md +40 -0
  49. package/package.json +1 -1
  50. package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
  51. package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
  52. package/src/auth/utils/__tests/validateCertificates.test.ts +12 -9
  53. package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
  54. package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
  55. package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
  56. package/src/kvstore/__tests/LocalKVStore.test.ts +4 -6
  57. package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
  58. package/src/overlay-tools/HostReputationTracker.ts +17 -14
  59. package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
  60. package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
  61. package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
  62. package/src/primitives/PrivateKey.ts +3 -3
  63. package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
  64. package/src/primitives/__tests/Curve.additional.test.ts +208 -0
  65. package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
  66. package/src/primitives/__tests/Hash.additional.test.ts +59 -0
  67. package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
  68. package/src/primitives/__tests/Point.additional.test.ts +503 -0
  69. package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
  70. package/src/primitives/__tests/Random.additional.test.ts +262 -0
  71. package/src/primitives/__tests/Signature.test.ts +333 -0
  72. package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
  73. package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
  74. package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
  75. package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
  76. package/src/script/Spend.ts +19 -11
  77. package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
  78. package/src/script/__tests/Script.additional.test.ts +100 -0
  79. package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
  80. package/src/script/__tests/Spend.additional.test.ts +837 -0
  81. package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
  82. package/src/storage/StorageDownloader.ts +6 -6
  83. package/src/storage/StorageUtils.ts +1 -1
  84. package/src/transaction/MerklePath.ts +196 -36
  85. package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
  86. package/src/transaction/__tests/Broadcaster.test.ts +159 -0
  87. package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
  88. package/src/transaction/__tests/MerklePath.test.ts +232 -21
  89. package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
  90. package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
  91. package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
  92. package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
  93. package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
  94. package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
  95. package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
  96. package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
  97. package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
  98. package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
  99. package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
  100. package/src/wallet/__tests/WERR.test.ts +212 -0
  101. package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
  102. package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
  103. package/src/wallet/__tests/WalletError.test.ts +290 -0
  104. package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
  105. package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
  106. package/src/wallet/substrates/__tests/HTTPWalletWire.test.ts +273 -0
@@ -0,0 +1,585 @@
1
+ import ARC from '../../../transaction/broadcasters/ARC'
2
+ import Transaction from '../../../transaction/Transaction'
3
+ import { FetchHttpClient } from '../../../transaction/http/FetchHttpClient'
4
+ import { NodejsHttpClient } from '../../../transaction/http/NodejsHttpClient'
5
+ import { HttpClientRequestOptions } from '../../http'
6
+ import { RequestOptions } from 'https'
7
+
8
+ // Mock Transaction
9
+ jest.mock('../../../transaction/Transaction', () => {
10
+ class MockTransaction {
11
+ toHex (): string {
12
+ return 'mocked_transaction_hex'
13
+ }
14
+
15
+ toHexEF (): string {
16
+ return 'mocked_transaction_hexEF'
17
+ }
18
+ }
19
+ return { __esModule: true, default: MockTransaction }
20
+ })
21
+
22
+ // ---- helpers ----------------------------------------------------------------
23
+
24
+ function mockedFetch (response: { status: number, data: any }): jest.Mock {
25
+ return jest.fn().mockResolvedValue({
26
+ ok: response.status >= 200 && response.status < 300,
27
+ status: response.status,
28
+ statusText: response.status === 200 ? 'OK' : 'Bad request',
29
+ headers: {
30
+ get: (key: string): string | undefined => {
31
+ if (key === 'Content-Type') return 'application/json; charset=UTF-8'
32
+ return undefined
33
+ }
34
+ },
35
+ json: async () => response.data
36
+ })
37
+ }
38
+
39
+ function mockedHttps (response: { status: number, data: any }): {
40
+ request: (
41
+ url: string,
42
+ options: RequestOptions,
43
+ callback: (res: {
44
+ statusCode: number
45
+ statusMessage: string
46
+ headers: { 'content-type': string }
47
+ on: (event: string, handler: (chunk?: any) => void) => void
48
+ }) => void
49
+ ) => { on: jest.Mock, write: jest.Mock, end: jest.Mock }
50
+ } {
51
+ const https = {
52
+ request: (
53
+ url: string,
54
+ options: RequestOptions,
55
+ callback: (res: any) => void
56
+ ) => {
57
+ const mockResponse = {
58
+ statusCode: response.status,
59
+ statusMessage: response.status === 200 ? 'OK' : 'Bad request',
60
+ headers: { 'content-type': 'application/json; charset=UTF-8' },
61
+ on (event: string, handler: (chunk?: any) => void) {
62
+ if (event === 'data') handler(JSON.stringify(response.data))
63
+ if (event === 'end') handler()
64
+ }
65
+ }
66
+ process.nextTick(() => callback(mockResponse))
67
+ return { on: jest.fn(), write: jest.fn(), end: jest.fn() }
68
+ }
69
+ }
70
+ jest.mock('https', () => https)
71
+ return https
72
+ }
73
+
74
+ // ---- suite ------------------------------------------------------------------
75
+
76
+ describe('ARC Broadcaster – additional coverage', () => {
77
+ const URL = 'https://arc.example.com'
78
+ let transaction: Transaction
79
+
80
+ beforeEach(() => {
81
+ transaction = new Transaction()
82
+ })
83
+
84
+ // --------------------------------------------------------------------------
85
+ // Constructor branches
86
+ // --------------------------------------------------------------------------
87
+
88
+ describe('constructor', () => {
89
+ it('sets callbackUrl and callbackToken on headers when provided via config', async () => {
90
+ const mockFetch = mockedFetch({ status: 200, data: { txid: 'abc', txStatus: 'SEEN_ON_NETWORK', extraInfo: '' } })
91
+ const broadcaster = new ARC(URL, {
92
+ callbackUrl: 'https://my.callback.url',
93
+ callbackToken: 'my-secret-token',
94
+ httpClient: new FetchHttpClient(mockFetch)
95
+ })
96
+ await broadcaster.broadcast(transaction)
97
+
98
+ const headers = (mockFetch.mock.calls[0][1] as HttpClientRequestOptions)?.headers ?? {}
99
+ expect(headers['X-CallbackUrl']).toBe('https://my.callback.url')
100
+ expect(headers['X-CallbackToken']).toBe('my-secret-token')
101
+ })
102
+
103
+ it('does not add X-CallbackUrl header when callbackUrl is empty string', async () => {
104
+ const mockFetch = mockedFetch({ status: 200, data: { txid: 'abc', txStatus: 'SEEN_ON_NETWORK', extraInfo: '' } })
105
+ const broadcaster = new ARC(URL, {
106
+ callbackUrl: '',
107
+ httpClient: new FetchHttpClient(mockFetch)
108
+ })
109
+ await broadcaster.broadcast(transaction)
110
+
111
+ const headers = (mockFetch.mock.calls[0][1] as HttpClientRequestOptions)?.headers ?? {}
112
+ expect(headers['X-CallbackUrl']).toBeUndefined()
113
+ })
114
+
115
+ it('does not add X-CallbackToken header when callbackToken is empty string', async () => {
116
+ const mockFetch = mockedFetch({ status: 200, data: { txid: 'abc', txStatus: 'SEEN_ON_NETWORK', extraInfo: '' } })
117
+ const broadcaster = new ARC(URL, {
118
+ callbackToken: '',
119
+ httpClient: new FetchHttpClient(mockFetch)
120
+ })
121
+ await broadcaster.broadcast(transaction)
122
+
123
+ const headers = (mockFetch.mock.calls[0][1] as HttpClientRequestOptions)?.headers ?? {}
124
+ expect(headers['X-CallbackToken']).toBeUndefined()
125
+ })
126
+
127
+ it('merges custom headers into request headers', async () => {
128
+ const mockFetch = mockedFetch({ status: 200, data: { txid: 'abc', txStatus: 'SEEN_ON_NETWORK', extraInfo: '' } })
129
+ const broadcaster = new ARC(URL, {
130
+ headers: { 'X-Custom-Header': 'custom-value', 'X-Another': 'another' },
131
+ httpClient: new FetchHttpClient(mockFetch)
132
+ })
133
+ await broadcaster.broadcast(transaction)
134
+
135
+ const headers = (mockFetch.mock.calls[0][1] as HttpClientRequestOptions)?.headers ?? {}
136
+ expect(headers['X-Custom-Header']).toBe('custom-value')
137
+ expect(headers['X-Another']).toBe('another')
138
+ // Standard headers still present
139
+ expect(headers['Content-Type']).toBe('application/json')
140
+ })
141
+
142
+ it('does not add Authorization header when apiKey is empty string', async () => {
143
+ const mockFetch = mockedFetch({ status: 200, data: { txid: 'abc', txStatus: 'SEEN_ON_NETWORK', extraInfo: '' } })
144
+ const broadcaster = new ARC(URL, {
145
+ apiKey: '',
146
+ httpClient: new FetchHttpClient(mockFetch)
147
+ })
148
+ await broadcaster.broadcast(transaction)
149
+
150
+ const headers = (mockFetch.mock.calls[0][1] as HttpClientRequestOptions)?.headers ?? {}
151
+ expect(headers.Authorization).toBeUndefined()
152
+ })
153
+
154
+ it('accepts config with no httpClient (uses default)', () => {
155
+ // Just verify construction does not throw
156
+ expect(() => new ARC(URL, {})).not.toThrow()
157
+ })
158
+ })
159
+
160
+ // --------------------------------------------------------------------------
161
+ // broadcast – error txStatus branches
162
+ // --------------------------------------------------------------------------
163
+
164
+ describe('broadcast – HTTP 200 error statuses', () => {
165
+ it('returns error for INVALID txStatus', async () => {
166
+ const mockFetch = mockedFetch({
167
+ status: 200,
168
+ data: { txid: 'txid1', txStatus: 'INVALID', extraInfo: 'script error' }
169
+ })
170
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
171
+ const response = await broadcaster.broadcast(transaction)
172
+
173
+ expect(response.status).toBe('error')
174
+ if (response.status === 'error') {
175
+ expect(response.code).toBe('INVALID')
176
+ expect(response.description).toContain('INVALID')
177
+ expect(response.description).toContain('script error')
178
+ expect(response.txid).toBe('txid1')
179
+ expect(response.more).toBeUndefined()
180
+ }
181
+ })
182
+
183
+ it('returns error for MALFORMED txStatus', async () => {
184
+ const mockFetch = mockedFetch({
185
+ status: 200,
186
+ data: { txid: 'txid2', txStatus: 'MALFORMED', extraInfo: 'bad format' }
187
+ })
188
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
189
+ const response = await broadcaster.broadcast(transaction)
190
+
191
+ expect(response.status).toBe('error')
192
+ if (response.status === 'error') {
193
+ expect(response.code).toBe('MALFORMED')
194
+ }
195
+ })
196
+
197
+ it('returns error for MINED_IN_STALE_BLOCK txStatus', async () => {
198
+ const mockFetch = mockedFetch({
199
+ status: 200,
200
+ data: { txid: 'txid3', txStatus: 'MINED_IN_STALE_BLOCK', extraInfo: '' }
201
+ })
202
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
203
+ const response = await broadcaster.broadcast(transaction)
204
+
205
+ expect(response.status).toBe('error')
206
+ if (response.status === 'error') {
207
+ expect(response.code).toBe('MINED_IN_STALE_BLOCK')
208
+ }
209
+ })
210
+
211
+ it('returns error when txStatus itself contains ORPHAN', async () => {
212
+ const mockFetch = mockedFetch({
213
+ status: 200,
214
+ data: { txid: 'orphanTxid', txStatus: 'SEEN_IN_ORPHAN_MEMPOOL', extraInfo: '' }
215
+ })
216
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
217
+ const response = await broadcaster.broadcast(transaction)
218
+
219
+ expect(response.status).toBe('error')
220
+ if (response.status === 'error') {
221
+ expect(response.code).toBe('SEEN_IN_ORPHAN_MEMPOOL')
222
+ expect(response.txid).toBe('orphanTxid')
223
+ }
224
+ })
225
+
226
+ it('includes competingTxs in failure when present on error txStatus', async () => {
227
+ const competingTxs = ['competingTx1', 'competingTx2']
228
+ const mockFetch = mockedFetch({
229
+ status: 200,
230
+ data: {
231
+ txid: 'txid4',
232
+ txStatus: 'REJECTED',
233
+ extraInfo: '',
234
+ competingTxs
235
+ }
236
+ })
237
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
238
+ const response = await broadcaster.broadcast(transaction)
239
+
240
+ expect(response.status).toBe('error')
241
+ if (response.status === 'error') {
242
+ expect(response.more).toEqual({ competingTxs })
243
+ }
244
+ })
245
+
246
+ it('includes competingTxs on successful broadcast when present', async () => {
247
+ const competingTxs = ['competingTx1']
248
+ const mockFetch = mockedFetch({
249
+ status: 200,
250
+ data: {
251
+ txid: 'successTxid',
252
+ txStatus: 'SEEN_ON_NETWORK',
253
+ extraInfo: 'ok',
254
+ competingTxs
255
+ }
256
+ })
257
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
258
+ const response = await broadcaster.broadcast(transaction)
259
+
260
+ expect(response.status).toBe('success')
261
+ if (response.status === 'success') {
262
+ expect(response.competingTxs).toEqual(competingTxs)
263
+ expect(response.txid).toBe('successTxid')
264
+ }
265
+ })
266
+
267
+ it('handles missing txStatus and extraInfo on successful response', async () => {
268
+ const mockFetch = mockedFetch({
269
+ status: 200,
270
+ data: { txid: 'minimalTxid' }
271
+ })
272
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
273
+ const response = await broadcaster.broadcast(transaction)
274
+
275
+ // No txStatus means no error status match – should succeed
276
+ expect(response.status).toBe('success')
277
+ if (response.status === 'success') {
278
+ expect(response.txid).toBe('minimalTxid')
279
+ // message should be 'undefined undefined' trimmed or similar
280
+ expect(typeof response.message).toBe('string')
281
+ }
282
+ })
283
+ })
284
+
285
+ // --------------------------------------------------------------------------
286
+ // broadcast – non-ok HTTP responses
287
+ // --------------------------------------------------------------------------
288
+
289
+ describe('broadcast – non-ok HTTP responses', () => {
290
+ it('handles non-ok response with object data containing txid and detail', async () => {
291
+ const mockFetch = mockedFetch({
292
+ status: 422,
293
+ data: {
294
+ txid: 'failedTxid',
295
+ detail: 'Unprocessable entity'
296
+ }
297
+ })
298
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
299
+ const response = await broadcaster.broadcast(transaction)
300
+
301
+ expect(response.status).toBe('error')
302
+ if (response.status === 'error') {
303
+ expect(response.code).toBe('422')
304
+ expect(response.txid).toBe('failedTxid')
305
+ expect(response.description).toBe('Unprocessable entity')
306
+ expect(response.more).toEqual({ txid: 'failedTxid', detail: 'Unprocessable entity' })
307
+ }
308
+ })
309
+
310
+ it('handles non-ok response with object data but no txid or detail', async () => {
311
+ const mockFetch = mockedFetch({
312
+ status: 500,
313
+ data: { someOtherField: 'value' }
314
+ })
315
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
316
+ const response = await broadcaster.broadcast(transaction)
317
+
318
+ expect(response.status).toBe('error')
319
+ if (response.status === 'error') {
320
+ expect(response.code).toBe('500')
321
+ expect(response.description).toBe('Unknown error')
322
+ expect(response.more).toEqual({ someOtherField: 'value' })
323
+ expect(response.txid).toBeUndefined()
324
+ }
325
+ })
326
+
327
+ it('handles non-ok response with null data', async () => {
328
+ const mockFetch = mockedFetch({ status: 503, data: null })
329
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
330
+ const response = await broadcaster.broadcast(transaction)
331
+
332
+ expect(response.status).toBe('error')
333
+ if (response.status === 'error') {
334
+ expect(response.code).toBe('503')
335
+ expect(response.description).toBe('Unknown error')
336
+ }
337
+ })
338
+
339
+ it('handles non-ok response with string data that is valid JSON', async () => {
340
+ const mockFetch = mockedFetch({
341
+ status: 400,
342
+ data: JSON.stringify({ detail: 'parsed from string', txid: 'parsedTxid' })
343
+ })
344
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
345
+ const response = await broadcaster.broadcast(transaction)
346
+
347
+ expect(response.status).toBe('error')
348
+ if (response.status === 'error') {
349
+ expect(response.description).toBe('parsed from string')
350
+ expect(response.txid).toBe('parsedTxid')
351
+ }
352
+ })
353
+
354
+ it('handles non-ok response with string data that is invalid JSON', async () => {
355
+ const mockFetch = mockedFetch({
356
+ status: 400,
357
+ data: 'not-valid-json-{'
358
+ })
359
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
360
+ const response = await broadcaster.broadcast(transaction)
361
+
362
+ // Should remain as 'Unknown error' since JSON parse fails
363
+ expect(response.status).toBe('error')
364
+ if (response.status === 'error') {
365
+ expect(response.code).toBe('400')
366
+ expect(response.description).toBe('Unknown error')
367
+ }
368
+ })
369
+
370
+ it('handles non-ok response where status type is neither number nor string', async () => {
371
+ // Craft a special mock that returns a non-string, non-number status
372
+ const mockFetch = jest.fn().mockResolvedValue({
373
+ ok: false,
374
+ status: undefined,
375
+ statusText: 'Unknown',
376
+ headers: { get: () => 'application/json' },
377
+ json: async () => null
378
+ })
379
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
380
+ const response = await broadcaster.broadcast(transaction)
381
+
382
+ expect(response.status).toBe('error')
383
+ if (response.status === 'error') {
384
+ expect(response.code).toBe('ERR_UNKNOWN')
385
+ }
386
+ })
387
+ })
388
+
389
+ // --------------------------------------------------------------------------
390
+ // broadcast – EF format fallback
391
+ // --------------------------------------------------------------------------
392
+
393
+ describe('broadcast – EF serialization fallback', () => {
394
+ it('falls back to toHex when toHexEF throws the expected EF error', async () => {
395
+ const mockFetch = mockedFetch({
396
+ status: 200,
397
+ data: { txid: 'efFallbackTxid', txStatus: 'SEEN_ON_NETWORK', extraInfo: '' }
398
+ })
399
+
400
+ // Override the mock transaction to throw the EF error
401
+ const mockTx = {
402
+ toHexEF: () => {
403
+ throw new Error('All inputs must have source transactions when serializing to EF format')
404
+ },
405
+ toHex: () => 'fallback_hex'
406
+ } as unknown as Transaction
407
+
408
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
409
+ const response = await broadcaster.broadcast(mockTx)
410
+
411
+ expect(response.status).toBe('success')
412
+ // Verify that the fallback hex was sent (not EF)
413
+ // FetchHttpClient serializes data into body via JSON.stringify, so parse it back
414
+ const sentData = JSON.parse((mockFetch.mock.calls[0][1] as any)?.body)
415
+ expect(sentData).toEqual({ rawTx: 'fallback_hex' })
416
+ })
417
+
418
+ it('re-throws non-EF errors from toHexEF', async () => {
419
+ const mockFetch = mockedFetch({ status: 200, data: {} })
420
+ const mockTx = {
421
+ toHexEF: () => {
422
+ throw new Error('Some other unexpected error')
423
+ },
424
+ toHex: () => 'fallback_hex'
425
+ } as unknown as Transaction
426
+
427
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
428
+ await expect(broadcaster.broadcast(mockTx)).rejects.toThrow('Some other unexpected error')
429
+ })
430
+ })
431
+
432
+ // --------------------------------------------------------------------------
433
+ // broadcast – catch block (network error)
434
+ // --------------------------------------------------------------------------
435
+
436
+ describe('broadcast – network-level errors', () => {
437
+ it('handles thrown error with non-string message', async () => {
438
+ const mockFetch = jest.fn().mockRejectedValue({ message: 42, toString: () => '42' })
439
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
440
+ const response = await broadcaster.broadcast(transaction)
441
+
442
+ expect(response.status).toBe('error')
443
+ if (response.status === 'error') {
444
+ expect(response.code).toBe('500')
445
+ expect(response.description).toBe('Internal Server Error')
446
+ }
447
+ })
448
+ })
449
+
450
+ // --------------------------------------------------------------------------
451
+ // broadcastMany
452
+ // --------------------------------------------------------------------------
453
+
454
+ describe('broadcastMany', () => {
455
+ it('broadcasts multiple transactions successfully', async () => {
456
+ const mockFetch = mockedFetch({
457
+ status: 200,
458
+ data: [
459
+ { txid: 'txid1', txStatus: 'SEEN_ON_NETWORK' },
460
+ { txid: 'txid2', txStatus: 'SEEN_ON_NETWORK' }
461
+ ]
462
+ })
463
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
464
+
465
+ const tx1 = new Transaction()
466
+ const tx2 = new Transaction()
467
+ const responses = await broadcaster.broadcastMany([tx1, tx2])
468
+
469
+ expect(mockFetch).toHaveBeenCalled()
470
+ expect(Array.isArray(responses)).toBe(true)
471
+ // Verify the URL used was /v1/txs
472
+ const calledUrl: string = mockFetch.mock.calls[0][0]
473
+ expect(calledUrl).toContain('/v1/txs')
474
+ })
475
+
476
+ it('sends array of rawTx objects to /v1/txs', async () => {
477
+ const mockFetch = mockedFetch({
478
+ status: 200,
479
+ data: [{ txid: 'txid1' }]
480
+ })
481
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
482
+ await broadcaster.broadcastMany([new Transaction()])
483
+
484
+ // FetchHttpClient serializes data into body via JSON.stringify, so parse it back
485
+ const sentData = JSON.parse((mockFetch.mock.calls[0][1] as any)?.body)
486
+ expect(Array.isArray(sentData)).toBe(true)
487
+ expect(sentData[0]).toHaveProperty('rawTx')
488
+ })
489
+
490
+ it('falls back to toHex for broadcastMany when toHexEF throws EF error', async () => {
491
+ const mockFetch = mockedFetch({
492
+ status: 200,
493
+ data: [{ txid: 'txid_ef_fallback' }]
494
+ })
495
+ const mockTx = {
496
+ toHexEF: () => {
497
+ throw new Error('All inputs must have source transactions when serializing to EF format')
498
+ },
499
+ toHex: () => 'non_ef_hex'
500
+ } as unknown as Transaction
501
+
502
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
503
+ const responses = await broadcaster.broadcastMany([mockTx])
504
+
505
+ // FetchHttpClient serializes data into body via JSON.stringify, so parse it back
506
+ const sentData = JSON.parse((mockFetch.mock.calls[0][1] as any)?.body) as any[]
507
+ expect(sentData[0]).toEqual({ rawTx: 'non_ef_hex' })
508
+ expect(Array.isArray(responses)).toBe(true)
509
+ })
510
+
511
+ it('re-throws non-EF errors from toHexEF in broadcastMany', async () => {
512
+ const mockFetch = mockedFetch({ status: 200, data: [] })
513
+ const mockTx = {
514
+ toHexEF: () => {
515
+ throw new Error('Unexpected serialization error')
516
+ },
517
+ toHex: () => 'fallback'
518
+ } as unknown as Transaction
519
+
520
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
521
+ await expect(broadcaster.broadcastMany([mockTx])).rejects.toThrow('Unexpected serialization error')
522
+ })
523
+
524
+ it('returns error objects for all transactions when HTTP request throws', async () => {
525
+ const mockFetch = jest.fn().mockRejectedValue(new Error('Connection refused'))
526
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
527
+
528
+ const tx1 = new Transaction()
529
+ const tx2 = new Transaction()
530
+ const responses = await broadcaster.broadcastMany([tx1, tx2])
531
+
532
+ expect(responses).toHaveLength(2)
533
+ for (const r of responses) {
534
+ const err = r as any
535
+ expect(err.status).toBe('error')
536
+ expect(err.code).toBe('500')
537
+ expect(err.description).toBe('Connection refused')
538
+ }
539
+ })
540
+
541
+ it('handles non-string error message in broadcastMany catch block', async () => {
542
+ const mockFetch = jest.fn().mockRejectedValue({ message: undefined })
543
+ const broadcaster = new ARC(URL, { httpClient: new FetchHttpClient(mockFetch) })
544
+
545
+ const responses = await broadcaster.broadcastMany([new Transaction()])
546
+ const err = responses[0] as any
547
+ expect(err.status).toBe('error')
548
+ expect(err.description).toBe('Internal Server Error')
549
+ })
550
+
551
+ it('sends correct request headers in broadcastMany', async () => {
552
+ const mockFetch = mockedFetch({ status: 200, data: [] })
553
+ const apiKey = 'test-api-key'
554
+ const broadcaster = new ARC(URL, {
555
+ apiKey,
556
+ httpClient: new FetchHttpClient(mockFetch)
557
+ })
558
+ await broadcaster.broadcastMany([new Transaction()])
559
+
560
+ const headers = (mockFetch.mock.calls[0][1] as HttpClientRequestOptions)?.headers ?? {}
561
+ expect(headers.Authorization).toBe(`Bearer ${apiKey}`)
562
+ expect(headers['Content-Type']).toBe('application/json')
563
+ expect(headers['XDeployment-ID']).toMatch(/ts-sdk-.*/)
564
+ })
565
+ })
566
+
567
+ // --------------------------------------------------------------------------
568
+ // Node.js https path for broadcastMany
569
+ // --------------------------------------------------------------------------
570
+
571
+ describe('broadcastMany – Node.js https', () => {
572
+ it('broadcasts multiple transactions using NodejsHttpClient', async () => {
573
+ const mockHttps = mockedHttps({
574
+ status: 200,
575
+ data: [{ txid: 'txid1' }, { txid: 'txid2' }]
576
+ })
577
+ const broadcaster = new ARC(URL, {
578
+ httpClient: new NodejsHttpClient(mockHttps)
579
+ })
580
+
581
+ const responses = await broadcaster.broadcastMany([new Transaction(), new Transaction()])
582
+ expect(Array.isArray(responses)).toBe(true)
583
+ })
584
+ })
585
+ })