@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.
- package/dist/cjs/package.json +1 -1
- package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js +827 -0
- package/dist/cjs/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
- package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +654 -0
- package/dist/cjs/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
- package/dist/cjs/src/transaction/MerklePath.js +132 -0
- package/dist/cjs/src/transaction/MerklePath.js.map +1 -1
- package/dist/cjs/tsconfig.cjs.tsbuildinfo +1 -1
- package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js +825 -0
- package/dist/esm/src/auth/clients/__tests__/AuthFetch.additional.test.js.map +1 -0
- package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js +619 -0
- package/dist/esm/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.js.map +1 -0
- package/dist/esm/src/transaction/MerklePath.js +132 -0
- package/dist/esm/src/transaction/MerklePath.js.map +1 -1
- package/dist/esm/tsconfig.esm.tsbuildinfo +1 -1
- package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts +21 -0
- package/dist/types/src/auth/clients/__tests__/AuthFetch.additional.test.d.ts.map +1 -0
- package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts +2 -0
- package/dist/types/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.d.ts.map +1 -0
- package/dist/types/src/transaction/MerklePath.d.ts +27 -0
- package/dist/types/src/transaction/MerklePath.d.ts.map +1 -1
- package/dist/types/tsconfig.types.tsbuildinfo +1 -1
- package/dist/umd/bundle.js +1 -1
- package/dist/umd/bundle.js.map +1 -1
- package/docs/reference/storage.md +1 -1
- package/docs/reference/transaction.md +40 -0
- package/package.json +1 -1
- package/src/auth/clients/__tests__/AuthFetch.additional.test.ts +1131 -0
- package/src/auth/transports/__tests__/SimplifiedFetchTransport.additional.test.ts +770 -0
- package/src/compat/__tests/Mnemonic.additional.test.ts +64 -0
- package/src/identity/__tests/IdentityClient.additional.test.ts +767 -0
- package/src/kvstore/__tests/LocalKVStore.additional.test.ts +611 -0
- package/src/kvstore/__tests/kvStoreInterpreter.test.ts +327 -0
- package/src/overlay-tools/__tests/HostReputationTracker.additional.test.ts +561 -0
- package/src/overlay-tools/__tests/LookupResolver.additional.test.ts +612 -0
- package/src/overlay-tools/__tests/withDoubleSpendRetry.test.ts +278 -0
- package/src/primitives/__tests/BigNumber.additional.test.ts +79 -0
- package/src/primitives/__tests/Curve.additional.test.ts +208 -0
- package/src/primitives/__tests/ECDSA.additional.test.ts +122 -0
- package/src/primitives/__tests/Hash.additional.test.ts +59 -0
- package/src/primitives/__tests/JacobianPoint.test.ts +308 -0
- package/src/primitives/__tests/Point.additional.test.ts +503 -0
- package/src/primitives/__tests/PublicKey.additional.test.ts +383 -0
- package/src/primitives/__tests/Random.additional.test.ts +262 -0
- package/src/primitives/__tests/Signature.test.ts +333 -0
- package/src/primitives/__tests/TransactionSignature.additional.test.ts +241 -0
- package/src/registry/__tests/RegistryClient.additional.test.ts +750 -0
- package/src/remittance/__tests/BasicBRC29.additional.test.ts +657 -0
- package/src/remittance/__tests/RemittanceManager.additional.test.ts +1272 -0
- package/src/script/__tests/LockingUnlockingScript.test.ts +79 -0
- package/src/script/__tests/Script.additional.test.ts +100 -0
- package/src/script/__tests/ScriptEvaluationError.test.ts +98 -0
- package/src/script/__tests/Spend.additional.test.ts +837 -0
- package/src/script/templates/__tests/RPuzzle.test.ts +134 -0
- package/src/transaction/MerklePath.ts +155 -0
- package/src/transaction/__tests/BeefParty.additional.test.ts +22 -0
- package/src/transaction/__tests/Broadcaster.test.ts +159 -0
- package/src/transaction/__tests/MerklePath.bench.test.ts +105 -0
- package/src/transaction/__tests/MerklePath.test.ts +80 -0
- package/src/transaction/__tests/Transaction.additional.test.ts +225 -0
- package/src/transaction/broadcasters/__tests/ARC.additional.test.ts +585 -0
- package/src/transaction/broadcasters/__tests/Teranode.test.ts +349 -0
- package/src/transaction/chaintrackers/__tests/BlockHeadersService.test.ts +253 -0
- package/src/transaction/chaintrackers/__tests/DefaultChainTracker.test.ts +44 -0
- package/src/transaction/chaintrackers/__tests/WhatsOnChain.additional.test.ts +193 -0
- package/src/transaction/fee-models/__tests/SatoshisPerKilobyte.test.ts +262 -0
- package/src/transaction/http/__tests/BinaryFetchClient.test.ts +212 -0
- package/src/transaction/http/__tests/DefaultHttpClient.additional.test.ts +192 -0
- package/src/transaction/http/__tests/DefaultHttpClient.test.ts +71 -0
- package/src/wallet/__tests/ProtoWallet.additional.test.ts +134 -0
- package/src/wallet/__tests/WERR.test.ts +212 -0
- package/src/wallet/__tests/WalletClient.additional.test.ts +699 -0
- package/src/wallet/__tests/WalletClient.substrate.test.ts +759 -0
- package/src/wallet/__tests/WalletError.test.ts +290 -0
- package/src/wallet/__tests/validationHelpers.test.ts +1218 -0
- package/src/wallet/substrates/__tests/HTTPWalletJSON.test.ts +496 -0
- 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
|
+
})
|