@tchain/tcabci-read-js-client 2.7.11 → 2.7.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/client.js CHANGED
@@ -1,3 +1,10 @@
1
+ import {
2
+ addresses as validateAddresses,
3
+ signedData as validateSignedData,
4
+ endpoint,
5
+ closeCode,
6
+ rfc3339Timestamp
7
+ } from './validation.js'
1
8
  import {
2
9
  ALREADY_CONNECTED,
3
10
  BLOCK_NOT_FOUND,
@@ -25,7 +32,7 @@ import { HTTP } from './http.js'
25
32
 
26
33
  /**
27
34
  * @callback SuccessCallback
28
- * @param {Transaction} event
35
+ * @param {Event} event
29
36
  * @return void
30
37
  */
31
38
 
@@ -50,14 +57,16 @@ import { HTTP } from './http.js'
50
57
  */
51
58
 
52
59
  export default class TCaBCIClient {
53
- _httpClient = new HTTP()
60
+ _httpClient
61
+ _startPromise = null
62
+ _generation = 0
54
63
  _subscribed = false
55
64
  _subscribedAddresses = []
56
65
  _SubscribedSignedData = {}
57
66
  _connected = false
58
67
  _chainName = 'transferchain'
59
68
  _chainVersion = 'v1'
60
- _version = `v2.7.11`
69
+ _version = `v2.7.13`
61
70
  /**
62
71
  * @type {?SuccessCallback}
63
72
  */
@@ -88,40 +97,66 @@ export default class TCaBCIClient {
88
97
  _readNodeWSAddress = READ_NODE_WS_ADDRESS
89
98
 
90
99
  /**
91
- * @param {Array<String>} readNodeAddresses
92
- * @param {WebSocket} wsLibrary
93
- * @param {?string} chainName
94
- * @param {?string} chainVersion
100
+ * Create an HTTP/WebSocket client. Start initiates connection setup;
101
+ * SetSuccessCallback reports when the socket is OPEN.
102
+ * @param {string[]} [readNodeAddresses=[]] HTTP and WS endpoints, or defaults.
103
+ * @param {import('./types.js').WebSocketConstructor} [wsLibrary=globalThis.WebSocket]
104
+ * @param {string|null} [chainName=null]
105
+ * @param {string|null} [chainVersion=null]
106
+ * @param {import('./types.js').ClientOptions} [options={}] Limits and TLS policy.
95
107
  */
96
108
  constructor(
97
109
  readNodeAddresses = [],
98
- wsLibrary,
110
+ wsLibrary = globalThis.WebSocket,
99
111
  chainName = null,
100
- chainVersion = null
112
+ chainVersion = null,
113
+ options = {}
101
114
  ) {
102
115
  if (!wsLibrary) throw new Error(INVALID_ARGUMENTS)
103
- this._wsLibrary = wsLibrary
104
116
 
105
- if (Array.isArray(readNodeAddresses) && readNodeAddresses.length === 2) {
106
- if (!readNodeAddresses[0].startsWith('http'))
107
- throw new Error(INVALID_ARGUMENTS)
108
- if (!readNodeAddresses[1].startsWith('ws'))
109
- throw new Error(INVALID_ARGUMENTS)
117
+ this._wsLibrary = wsLibrary
110
118
 
111
- this._readNodeAddress = readNodeAddresses[0]
112
- this._readNodeWSAddress = readNodeAddresses[1]
113
- this._httpClient.setBaseURL(readNodeAddresses[0])
119
+ if (
120
+ !Array.isArray(readNodeAddresses) ||
121
+ ![0, 2].includes(readNodeAddresses.length)
122
+ )
123
+ throw new Error(INVALID_ARGUMENTS)
124
+
125
+ if (readNodeAddresses.length === 2) {
126
+ this._readNodeAddress = endpoint(
127
+ readNodeAddresses[0],
128
+ ['https:', 'http:'],
129
+ options.allowInsecure
130
+ ).href
131
+ this._readNodeWSAddress = endpoint(
132
+ readNodeAddresses[1],
133
+ ['wss:', 'ws:'],
134
+ options.allowInsecure
135
+ ).href
114
136
  }
115
137
 
138
+ this._httpClient = new HTTP(this._readNodeAddress, options)
116
139
  this._options = new Options(this._readNodeWSAddress)
117
- this._options.setCustomWS(this._wsLibrary)
140
+ .setAllowInsecure(options.allowInsecure)
141
+ .setCustomWS(this._wsLibrary)
142
+
143
+ if (typeof options.maxMessageBytes !== 'undefined')
144
+ this._options.setMaxMessageBytes(options.maxMessageBytes)
145
+
146
+ if (typeof options.maxEnqueuedMessages !== 'undefined')
147
+ this._options.setMaxEnqueuedMessages(options.maxEnqueuedMessages)
148
+
149
+ if (typeof options.maxPendingCallbacks !== 'undefined')
150
+ this._options.setMaxPendingCallbacks(options.maxPendingCallbacks)
118
151
 
119
152
  if (chainName) this._chainName = chainName
153
+
120
154
  if (chainVersion) this._chainVersion = chainVersion
121
155
  }
122
156
 
123
157
  SetDebug(debug) {
124
158
  this._options.setDebug(debug)
159
+
125
160
  return this
126
161
  }
127
162
 
@@ -130,6 +165,9 @@ export default class TCaBCIClient {
130
165
  * @return {TCaBCIClient}
131
166
  */
132
167
  SetSuccessCallback(cb) {
168
+ if (cb !== null && typeof cb !== 'function')
169
+ throw new TypeError('Invalid callback')
170
+
133
171
  this._successCb = cb
134
172
 
135
173
  return this
@@ -140,6 +178,9 @@ export default class TCaBCIClient {
140
178
  * @return {TCaBCIClient}
141
179
  */
142
180
  SetErrorCallback(cb) {
181
+ if (cb !== null && typeof cb !== 'function')
182
+ throw new TypeError('Invalid callback')
183
+
143
184
  this._errorCb = cb
144
185
 
145
186
  return this
@@ -150,6 +191,9 @@ export default class TCaBCIClient {
150
191
  * @return {TCaBCIClient}
151
192
  */
152
193
  SetCloseCallback(cb) {
194
+ if (cb !== null && typeof cb !== 'function')
195
+ throw new TypeError('Invalid callback')
196
+
153
197
  this._closeCb = cb
154
198
 
155
199
  return this
@@ -160,6 +204,9 @@ export default class TCaBCIClient {
160
204
  * @return {TCaBCIClient}
161
205
  */
162
206
  SetListenCallback(cb) {
207
+ if (cb !== null && typeof cb !== 'function')
208
+ throw new TypeError('Invalid callback')
209
+
163
210
  this._listenCb = cb
164
211
 
165
212
  return this
@@ -173,42 +220,55 @@ export default class TCaBCIClient {
173
220
  return this._subscribed
174
221
  }
175
222
 
223
+ /** @returns {ReadonlyArray<string>} Frozen address copy. */
176
224
  get SubscribeAddresses() {
177
- return this._subscribedAddresses
225
+ return Object.freeze([...this._subscribedAddresses])
178
226
  }
179
227
 
228
+ /** @returns {Readonly<Record<string, string>>} Frozen proof copy. */
180
229
  get SubscribedSignedData() {
181
- return this._SubscribedSignedData
230
+ return Object.freeze({ ...this._SubscribedSignedData })
182
231
  }
183
232
 
184
233
  get Socket() {
185
234
  return this._ws
186
235
  }
187
236
 
237
+ /**
238
+ * Stop the old socket and initiate a replacement connection.
239
+ * @param {number} [code=1000]
240
+ * @returns {Promise<TCaBCIClient>}
241
+ */
188
242
  async Reconnect(code = 1000) {
189
- if (this.IsConnected()) {
190
- await this._disconnect(code)
191
- }
243
+ const stopping = this._disconnect(code),
244
+ generation = this._generation
245
+
246
+ await stopping
247
+
248
+ if (generation !== this._generation) return this
192
249
 
193
250
  await this._connect()
194
251
 
195
252
  return this
196
253
  }
197
254
 
255
+ /**
256
+ * Initiate one shared connection attempt; does not await the OPEN event.
257
+ * @returns {Promise<TWebSocket>}
258
+ */
198
259
  async Start() {
199
260
  return this._connect()
200
261
  }
201
262
 
202
263
  /**
203
- * @param {?number} code
204
- * @return {Awaited<TCaBCIClient>}
205
- * @throws {Error}
264
+ * Idempotently stop sockets, abort HTTP and release subscription state.
265
+ * Application callbacks remain registered for reuse; Dispose releases them.
266
+ * Caller-owned buffers are never zeroed or frozen.
267
+ * @param {number} [code=1000]
268
+ * @returns {Promise<TCaBCIClient>}
206
269
  */
207
270
  async Stop(code = 1000) {
208
- if (!this.IsConnected()) {
209
- throw new Error(NOT_CONNECTED)
210
- }
211
-
271
+ this._httpClient.abort()
212
272
  await this._disconnect(code)
213
273
  this._setConnected(false)
214
274
  this._setSubscribed(false)
@@ -216,69 +276,56 @@ export default class TCaBCIClient {
216
276
  return this
217
277
  }
218
278
 
279
+ /**
280
+ * Return a detached, frozen status snapshot.
281
+ * @returns {Readonly<import('./client.js').ClientStatus>}
282
+ */
219
283
  Status() {
220
- return {
284
+ return Object.freeze({
221
285
  chain_name: this._chainName,
222
286
  chain_version: this._chainVersion,
223
287
  connected: this._connected,
224
288
  subscribed: this._subscribed
225
- }
289
+ })
226
290
  }
227
291
 
228
292
  /**
229
- * @param {Array<string>} addresses
230
- * @param {Object} signedData
231
- * @param {?Array<string>} txTypes
232
- * @return {TCaBCIClient}
233
- * @throws {Error}
293
+ * Subscribe to at most 251 unique addresses, each at most 2048 characters.
294
+ * The combined active address list is bounded. Proof maps are copied;
295
+ * caller objects remain mutable. ACK updates IsSubscribed asynchronously.
296
+ * @param {readonly string[]} addresses
297
+ * @param {Readonly<Record<string, string>>} signedData
298
+ * @param {readonly string[]|null} [txTypes=null]
299
+ * @returns {TCaBCIClient}
234
300
  */
235
301
  Subscribe(addresses, signedData, txTypes = null) {
236
- if (!Array.isArray(addresses) || addresses.length === 0)
237
- throw new Error(INVALID_ARGUMENT_WITH_CS('addresses'))
238
-
239
- if (typeof signedData !== 'object') throw new Error(INVALID_ARGUMENTS)
240
-
241
- if (txTypes && Array.isArray(txTypes)) {
242
- if (!Array.isArray(txTypes))
243
- throw new Error(INVALID_ARGUMENT_WITH_CS('txTypes'))
244
-
245
- if (txTypes.length > TX_TYPE_LIST.length)
246
- throw new Error(INVALID_ARGUMENT_WITH_CS('txTypes'))
247
-
248
- for (let i = 0; i < txTypes.length; i++) {
249
- if (!TX_TYPE_LIST.includes(txTypes[i]))
250
- throw new Error(INVALID_ARGUMENT_WITH_CS('txType ', txTypes[i]))
251
- }
252
- }
302
+ const requested = validateAddresses(addresses),
303
+ proof = validateSignedData(signedData)
304
+
305
+ if (
306
+ txTypes !== null &&
307
+ (!Array.isArray(txTypes) ||
308
+ txTypes.length > TX_TYPE_LIST.length ||
309
+ txTypes.some((type) => !TX_TYPE_LIST.includes(type)))
310
+ )
311
+ throw new Error(INVALID_ARGUMENT_WITH_CS('txTypes'))
253
312
 
254
313
  if (!this.IsConnected()) throw new Error(NOT_CONNECTED)
255
314
 
256
- const addrs = [],
257
- _signedData = signedData ?? this.SubscribedSignedData
258
-
259
- addrs.push(...addresses)
260
-
261
- if (this.SubscribeAddresses.length > 0) {
262
- let newAddress = []
263
-
264
- for (let i = 0; i < addresses.length; i++) {
265
- if (this.SubscribeAddresses.indexOf(addresses[i]) === -1) {
266
- newAddress.push(addresses[i])
267
- }
268
- }
269
- addrs.push(...newAddress)
270
- }
315
+ const addrs = validateAddresses([
316
+ ...new Set([...this._subscribedAddresses, ...requested])
317
+ ])
271
318
 
272
319
  this._ws.send(
273
320
  new Message({
274
321
  isWeb: true,
275
322
  type: SUBSCRIBEMessage,
276
323
  addrs: addrs,
277
- signedData: _signedData,
324
+ signedData: proof,
278
325
  txTypes
279
326
  }).ToJSON()
280
327
  )
281
- this._setSubscribeAddresses(addrs, true)._setSubscribeSignedData(signedData)
328
+ this._setSubscribeAddresses(addrs)._setSubscribeSignedData(proof)
282
329
 
283
330
  return this
284
331
  }
@@ -290,108 +337,122 @@ export default class TCaBCIClient {
290
337
  if (!this.IsSubscribed()) {
291
338
  throw new Error(NOT_SUBSCRIBED)
292
339
  }
293
- this._ws.send(
294
- new Message({
295
- isWeb: true,
296
- type: UNSUBSCRIBEMessage,
297
- addrs: this.SubscribeAddresses
298
- }).ToJSON()
299
- )
300
- this._setSubscribed(false)
301
- this._setSubscribeAddresses([])
302
- this._setSubscribeSignedData({})
340
+
341
+ try {
342
+ this._ws.send(
343
+ new Message({
344
+ isWeb: true,
345
+ type: UNSUBSCRIBEMessage,
346
+ addrs: this.SubscribeAddresses
347
+ }).ToJSON()
348
+ )
349
+ } finally {
350
+ this._clearSubscription()
351
+ }
303
352
 
304
353
  return this
305
354
  }
306
355
 
307
356
  /**
308
- * @param {?string} chainName
309
- * @param {?string} chainVersion
310
- * @return {Promise<{blocks: [], info: {chain_name: null, chain_version: null, hash: *, height: *, txs: *, created_at: *}, total_count: *} | Error>}
311
- * @constructor
357
+ * Read the latest block; query values are URL-encoded.
358
+ * @param {string|null} [chainName=null]
359
+ * @param {string|null} [chainVersion=null]
360
+ * @param {import('./types.js').RequestOptions} [requestOptions={}]
361
+ * @returns {Promise<Readonly<Awaited<ReturnType<import('./client.js').default['LastBlock']>>>>} Frozen result; nested snapshots are readonly.
312
362
  */
313
- LastBlock(chainName = null, chainVersion = null) {
314
- return this._httpClient
315
- .request(
316
- `/v1/blocks?chain_name=${chainName ?? this._chainName}&chain_version=${chainVersion ?? this._chainVersion}&limit=1&offset=0`,
317
- {
318
- method: 'GET'
319
- }
320
- )
321
- .then(res => {
322
- return {
323
- blocks: [],
324
- info: {
325
- chainName: chainName,
326
- chainVersion: chainVersion,
363
+ LastBlock(chainName = null, chainVersion = null, requestOptions = {}) {
364
+ return this._request(
365
+ `/v1/blocks?chain_name=${encodeURIComponent(chainName ?? this._chainName)}&chain_version=${encodeURIComponent(chainVersion ?? this._chainVersion)}&limit=1&offset=0`,
366
+ {
367
+ method: 'GET'
368
+ },
369
+ requestOptions
370
+ )
371
+ .then((res) => {
372
+ return Object.freeze({
373
+ blocks: Object.freeze([]),
374
+ info: Object.freeze({
375
+ chainName: chainName ?? this._chainName,
376
+ chainVersion: chainVersion ?? this._chainVersion,
327
377
  hash: res.data.hash,
328
378
  height: res.data.height,
329
379
  txs: res.data.txs,
330
- createdAt: new Date(res.data.inserted_at)
331
- },
380
+ createdAt: new Date(rfc3339Timestamp(res.data.inserted_at))
381
+ }),
332
382
  total_count: res.total_count
333
- }
383
+ })
334
384
  })
335
- .catch(e => this._httpClient.handleError(e, new Error(BLOCK_NOT_FOUND)))
385
+ .catch((e) => this._httpClient.handleError(e, new Error(BLOCK_NOT_FOUND)))
336
386
  }
337
387
 
338
388
  /**
389
+ * Read a transaction. Signature headers are never followed across redirects.
339
390
  * @param {string} id
340
391
  * @param {string} signature
341
- * @return {Promise<*>}
392
+ * @param {import('./types.js').RequestOptions} [requestOptions={}]
393
+ * @returns {Promise<Readonly<{tx: Readonly<Transaction>}>>} Frozen result; nested snapshots are readonly.
342
394
  */
343
- Tx(id, signature) {
344
- if (!id || typeof id !== 'string') {
395
+ Tx(id, signature, requestOptions = {}) {
396
+ if (
397
+ !id ||
398
+ id === '.' ||
399
+ id === '..' ||
400
+ typeof id !== 'string' ||
401
+ typeof signature !== 'string' ||
402
+ !signature.length
403
+ ) {
345
404
  return Promise.reject(new Error(INVALID_ARGUMENTS))
346
405
  }
347
406
 
348
- return this._httpClient
349
- .request(`/v1/tx/${id}`, {
407
+ return this._request(
408
+ `/v1/tx/${encodeURIComponent(id)}`,
409
+ {
350
410
  method: 'GET',
351
411
  headers: { 'X-Signature': signature }
352
- })
353
- .then(res => {
412
+ },
413
+ requestOptions
414
+ )
415
+ .then((res) => {
354
416
  const { transaction, error } = Transaction.FromObject(res.data)
417
+
355
418
  if (error) return Promise.reject(error)
356
419
 
357
- return { tx: transaction }
420
+ return Object.freeze({ tx: transaction })
358
421
  })
359
- .catch(e => this._httpClient.handleError(e))
422
+ .catch((e) => this._httpClient.handleError(e))
360
423
  }
361
424
 
362
425
  /**
363
- * @param {?Array<string>} recipientAddrs
364
- * @param {?Array<string>} senderAddrs
365
- * @param {?Object} signedData
366
- * @param {?string} typ
367
- * @param {?Array<string>} types
368
- * @param {?string} chainName
369
- * @param {?string} chainVersion
370
- * @return {Promise<{
371
- * chain_name: string,
372
- * chain_version: string,
373
- * first_block_height: number,
374
- * first_transaction: Transaction,
375
- * last_block_height: number,
376
- * last_transaction: Transaction,
377
- * total_count: number,
378
- * }>}
426
+ * Read transaction data with cancellation and a total request deadline.
427
+ * @param {import('./client.js').TxQuery} query
428
+ * @param {import('./types.js').RequestOptions} [requestOptions={}]
429
+ * @returns {Promise<Readonly<Awaited<ReturnType<import('./client.js').default['TxSummary']>>>>} Frozen result; nested snapshots are readonly.
379
430
  */
380
- TxSummary({
381
- recipientAddrs,
382
- senderAddrs,
383
- signedData,
384
- typ,
385
- types = null,
386
- chainName = null,
387
- chainVersion = null
388
- }) {
431
+ TxSummary(
432
+ {
433
+ recipientAddrs,
434
+ senderAddrs,
435
+ signedData,
436
+ typ,
437
+ types = null,
438
+ chainName = null,
439
+ chainVersion = null
440
+ },
441
+ requestOptions = {}
442
+ ) {
389
443
  if (!recipientAddrs && !senderAddrs) {
390
444
  return Promise.reject(new Error(INVALID_ARGUMENTS))
391
445
  }
392
446
 
393
- return this._httpClient
394
- .request('/v1/tx_summary', {
447
+ if (recipientAddrs) recipientAddrs = validateAddresses(recipientAddrs)
448
+
449
+ if (senderAddrs) senderAddrs = validateAddresses(senderAddrs)
450
+
451
+ if (signedData) signedData = validateSignedData(signedData)
452
+
453
+ return this._request(
454
+ '/v1/tx_summary',
455
+ {
395
456
  method: 'POST',
396
457
  body: JSON.stringify({
397
458
  chain_name: chainName ?? this._chainName,
@@ -401,15 +462,19 @@ export default class TCaBCIClient {
401
462
  signed_addrs: signedData,
402
463
  ...(types ? { types: types } : { typ: typ })
403
464
  })
404
- })
405
- .then(res => {
465
+ },
466
+ requestOptions
467
+ )
468
+ .then((res) => {
406
469
  let firstTransaction, lastTransaction
407
470
 
408
471
  if (res.data.first_transaction) {
409
472
  const { transaction, error } = Transaction.FromObject(
410
473
  res.data.first_transaction
411
474
  )
475
+
412
476
  if (error) return Promise.reject(error)
477
+
413
478
  firstTransaction = transaction
414
479
  }
415
480
 
@@ -417,11 +482,13 @@ export default class TCaBCIClient {
417
482
  const { transaction, error: errorTwo } = Transaction.FromObject(
418
483
  res.data.last_transaction
419
484
  )
485
+
420
486
  if (errorTwo) return Promise.reject(errorTwo)
487
+
421
488
  lastTransaction = transaction
422
489
  }
423
490
 
424
- return {
491
+ return Object.freeze({
425
492
  chain_name: res.data.chain_name,
426
493
  chain_version: res.data.chain_version,
427
494
  first_block_height: res.data.first_block_height,
@@ -429,50 +496,47 @@ export default class TCaBCIClient {
429
496
  last_block_height: res.data.last_block_height,
430
497
  last_transaction: lastTransaction,
431
498
  total_count: res.total_count
432
- }
499
+ })
433
500
  })
434
- .catch(e => this._httpClient.handleError(e))
501
+ .catch((e) => this._httpClient.handleError(e))
435
502
  }
436
503
 
437
504
  /**
438
- * @param {string} heightOperator
439
- * @param {number} height
440
- * @param {?number} maxHeight
441
- * @param {?number} lastOrder
442
- * @param {?Array<string>} recipientAddrs
443
- * @param {?Array<string>} senderAddrs
444
- * @param {?Object} signedData
445
- * @param {?Array<string>} hashes
446
- * @param {string} typ
447
- * @param {?Array<string>} types
448
- * @param {number} limit
449
- * @param {number} offset
450
- * @param {string} orderField
451
- * @param {string} orderBy
452
- * @param {?string} chainName
453
- * @param {?string} chainVersion
454
- * @return {Promise<{txs: Transaction[], total_count: number}>}
505
+ * Read transaction data with cancellation and a total request deadline.
506
+ * @param {import('./client.js').TxSearchQuery} query
507
+ * @param {import('./types.js').RequestOptions} [requestOptions={}]
508
+ * @returns {Promise<Readonly<Awaited<ReturnType<import('./client.js').default['TxSearch']>>>>} Frozen result; nested snapshots are readonly.
455
509
  */
456
- TxSearch({
457
- heightOperator,
458
- height,
459
- maxHeight,
460
- lastOrder,
461
- recipientAddrs,
462
- senderAddrs,
463
- signedData,
464
- hashes,
465
- typ,
466
- types,
467
- limit,
468
- offset,
469
- orderField,
470
- orderBy,
471
- chainName = null,
472
- chainVersion = null
473
- }) {
474
- return this._httpClient
475
- .request('/v1/tx_search/p', {
510
+ TxSearch(
511
+ {
512
+ heightOperator,
513
+ height,
514
+ maxHeight,
515
+ lastOrder,
516
+ recipientAddrs,
517
+ senderAddrs,
518
+ signedData,
519
+ hashes,
520
+ typ,
521
+ types,
522
+ limit,
523
+ offset,
524
+ orderField,
525
+ orderBy,
526
+ chainName = null,
527
+ chainVersion = null
528
+ },
529
+ requestOptions = {}
530
+ ) {
531
+ if (recipientAddrs) recipientAddrs = validateAddresses(recipientAddrs)
532
+
533
+ if (senderAddrs) senderAddrs = validateAddresses(senderAddrs)
534
+
535
+ if (signedData) signedData = validateSignedData(signedData)
536
+
537
+ return this._request(
538
+ '/v1/tx_search/p',
539
+ {
476
540
  method: 'POST',
477
541
  body: JSON.stringify({
478
542
  chain_name: chainName ?? this._chainName,
@@ -490,45 +554,38 @@ export default class TCaBCIClient {
490
554
  ...(orderBy ? { order_by: orderBy } : {}),
491
555
  ...(types ? { types: types } : typ ? { typ: typ } : {})
492
556
  })
493
- })
494
- .then(res => {
557
+ },
558
+ requestOptions
559
+ )
560
+ .then((res) => {
495
561
  const data = []
496
562
 
497
563
  for (const _data of res.data) {
498
564
  const { transaction, error } = Transaction.FromObject(_data)
565
+
499
566
  if (error) return Promise.reject(error)
567
+
500
568
  data.push(transaction)
501
569
  }
502
570
 
503
- return {
504
- txs: data,
571
+ return Object.freeze({
572
+ txs: Object.freeze(data),
505
573
  total_count: res.total_count
506
- }
574
+ })
507
575
  })
508
- .catch(e => this._httpClient.handleError(e))
576
+ .catch((e) => this._httpClient.handleError(e))
509
577
  }
510
578
 
511
579
  /**
512
- * @param {string} id
513
- * @param {number} version
514
- * @param {string} type
515
- * @param {string} data
516
- * @param {string} sender_addr
517
- * @param {string} recipient_addr
518
- * @param {string} sign
519
- * @param {number} fee
520
- * @return {Promise<any>}
580
+ * Publish once: automatic retries are disabled to avoid duplicate writes.
581
+ * @param {import('./client.js').BroadcastInput} input
582
+ * @param {import('./types.js').RequestOptions} [requestOptions={}]
583
+ * @returns {Promise<Readonly<{data: import('./types.js').JSONValue}>>} Frozen result; nested snapshots are readonly.
521
584
  */
522
- BroadcastCommit({
523
- id,
524
- version,
525
- type,
526
- data,
527
- sender_addr,
528
- recipient_addr,
529
- sign,
530
- fee
531
- }) {
585
+ BroadcastCommit(
586
+ { id, version, type, data, sender_addr, recipient_addr, sign, fee },
587
+ requestOptions = {}
588
+ ) {
532
589
  return this.broadcast(
533
590
  {
534
591
  id,
@@ -541,35 +598,32 @@ export default class TCaBCIClient {
541
598
  fee
542
599
  },
543
600
  false,
544
- true
601
+ true,
602
+ requestOptions
545
603
  )
546
604
  }
547
605
 
548
606
  /**
549
- * @param {string} id
550
- * @param {number} version
551
- * @param {string} type
552
- * @param {string} data
553
- * @param {?string} additional_data
554
- * @param {?string} cipher_data
555
- * @param {string} sender_addr
556
- * @param {string} recipient_addr
557
- * @param {string} sign
558
- * @param {number} fee
559
- * @return {Promise<any>}
607
+ * Publish once: automatic retries are disabled to avoid duplicate writes.
608
+ * @param {import('./client.js').BroadcastInput} input
609
+ * @param {import('./types.js').RequestOptions} [requestOptions={}]
610
+ * @returns {Promise<Readonly<{data: import('./types.js').JSONValue}>>} Frozen result; nested snapshots are readonly.
560
611
  */
561
- BroadcastSync({
562
- id,
563
- version,
564
- type,
565
- data,
566
- additional_data = null,
567
- cipher_data = null,
568
- sender_addr,
569
- recipient_addr,
570
- sign,
571
- fee
572
- }) {
612
+ BroadcastSync(
613
+ {
614
+ id,
615
+ version,
616
+ type,
617
+ data,
618
+ additional_data = null,
619
+ cipher_data = null,
620
+ sender_addr,
621
+ recipient_addr,
622
+ sign,
623
+ fee
624
+ },
625
+ requestOptions = {}
626
+ ) {
573
627
  return this.broadcast(
574
628
  {
575
629
  id,
@@ -584,35 +638,32 @@ export default class TCaBCIClient {
584
638
  fee
585
639
  },
586
640
  true,
587
- false
641
+ false,
642
+ requestOptions
588
643
  )
589
644
  }
590
645
 
591
646
  /**
592
- * @param {string} id
593
- * @param {number} version
594
- * @param {string} type
595
- * @param {string} data
596
- * @param {?string} additional_data
597
- * @param {?string} cipher_data
598
- * @param {string} sender_addr
599
- * @param {string} recipient_addr
600
- * @param {string} sign
601
- * @param {number} fee
602
- * @return {Promise<any>}
647
+ * Publish once: automatic retries are disabled to avoid duplicate writes.
648
+ * @param {import('./client.js').BroadcastInput} input
649
+ * @param {import('./types.js').RequestOptions} [requestOptions={}]
650
+ * @returns {Promise<Readonly<{data: import('./types.js').JSONValue}>>} Frozen result; nested snapshots are readonly.
603
651
  */
604
- Broadcast({
605
- id,
606
- version,
607
- type,
608
- data,
609
- additional_data = null,
610
- cipher_data = null,
611
- sender_addr,
612
- recipient_addr,
613
- sign,
614
- fee
615
- }) {
652
+ Broadcast(
653
+ {
654
+ id,
655
+ version,
656
+ type,
657
+ data,
658
+ additional_data = null,
659
+ cipher_data = null,
660
+ sender_addr,
661
+ recipient_addr,
662
+ sign,
663
+ fee
664
+ },
665
+ requestOptions = {}
666
+ ) {
616
667
  return this.broadcast(
617
668
  {
618
669
  id,
@@ -627,10 +678,19 @@ export default class TCaBCIClient {
627
678
  fee
628
679
  },
629
680
  false,
630
- false
681
+ false,
682
+ requestOptions
631
683
  )
632
684
  }
633
685
 
686
+ /**
687
+ * Internal/public legacy broadcast entry; never retries a write.
688
+ * @param {import('./client.js').BroadcastInput} input
689
+ * @param {boolean} [sync=false]
690
+ * @param {boolean} [commit=false]
691
+ * @param {import('./types.js').RequestOptions} [requestOptions={}]
692
+ * @returns {Promise<Readonly<{data: import('./types.js').JSONValue}>>} Frozen result; nested snapshots are readonly.
693
+ */
634
694
  broadcast(
635
695
  {
636
696
  id,
@@ -645,14 +705,30 @@ export default class TCaBCIClient {
645
705
  fee
646
706
  },
647
707
  sync = false,
648
- commit = false
708
+ commit = false,
709
+ requestOptions = {}
649
710
  ) {
650
711
  if (!TX_TYPE_LIST.includes(type)) {
651
712
  throw new Error(TRANSACTION_TYPE_NOT_VALID)
652
713
  }
653
714
 
654
- return this._httpClient
655
- .request(commit ? '/v1/tx/commit' : sync ? '/v1/tx/sync' : '/v1/tx', {
715
+ validateAddresses([sender_addr, recipient_addr])
716
+
717
+ if (
718
+ typeof id !== 'string' ||
719
+ !id.length ||
720
+ typeof sign !== 'string' ||
721
+ !sign.length ||
722
+ !Number.isSafeInteger(version) ||
723
+ version < 0 ||
724
+ !Number.isFinite(fee) ||
725
+ fee < 0
726
+ )
727
+ throw new Error(INVALID_ARGUMENTS)
728
+
729
+ return this._request(
730
+ commit ? '/v1/tx/commit' : sync ? '/v1/tx/sync' : '/v1/tx',
731
+ {
656
732
  method: 'POST',
657
733
  body: JSON.stringify({
658
734
  id,
@@ -666,11 +742,13 @@ export default class TCaBCIClient {
666
742
  sign,
667
743
  fee
668
744
  })
745
+ },
746
+ requestOptions
747
+ )
748
+ .then((res) => {
749
+ return Object.freeze({ data: res.data })
669
750
  })
670
- .then(res => {
671
- return { data: res.data }
672
- })
673
- .catch(e =>
751
+ .catch((e) =>
674
752
  this._httpClient.handleError(e, {
675
753
  400: new Error(TRANSACTION_NOT_BROADCAST)
676
754
  })
@@ -678,86 +756,144 @@ export default class TCaBCIClient {
678
756
  }
679
757
 
680
758
  /**
681
- * @param {Array<string>} addresses
682
- * @param {Object} signedData
683
- * @param {?number} maxHeight
684
- * @param {?string} chainName
685
- * @param {?string} chainVersion
686
- * @return {Promise<*>}
687
- * @constructor
759
+ * Read a bounded address batch (1..251 entries; 1..2048 chars per address).
760
+ * @param {readonly string[]} [addresses=[]]
761
+ * @param {Readonly<Record<string, string>>} [signedData={}]
762
+ * @param {number|null} [maxHeight=null]
763
+ * @param {string|null} [chainName=null]
764
+ * @param {string|null} [chainVersion=null]
765
+ * @param {import('./types.js').RequestOptions} [requestOptions={}]
766
+ * @returns {Promise<import('./types.js').JSONValue>} Recursively readonly JSON snapshot.
688
767
  */
689
768
  Bulk(
690
769
  addresses = [],
691
770
  signedData = {},
692
771
  maxHeight = null,
693
772
  chainName = null,
694
- chainVersion = null
773
+ chainVersion = null,
774
+ requestOptions = {}
695
775
  ) {
696
- return this._httpClient.request('/v1/bulk_tx', {
697
- method: 'POST',
698
- body: JSON.stringify({
699
- chain_name: chainName ?? this._chainName,
700
- chain_version: chainVersion ?? this._chainVersion,
701
- addresses: addresses,
702
- signed_addrs: signedData,
703
- ...(maxHeight ? { max_height: maxHeight } : {})
704
- })
705
- })
776
+ addresses = validateAddresses(addresses)
777
+ signedData = validateSignedData(signedData)
778
+
779
+ return this._request(
780
+ '/v1/bulk_tx',
781
+ {
782
+ method: 'POST',
783
+ body: JSON.stringify({
784
+ chain_name: chainName ?? this._chainName,
785
+ chain_version: chainVersion ?? this._chainVersion,
786
+ addresses: addresses,
787
+ signed_addrs: signedData,
788
+ ...(maxHeight ? { max_height: maxHeight } : {})
789
+ })
790
+ },
791
+ requestOptions
792
+ )
706
793
  }
707
794
 
708
795
  /**
709
796
  * @return {Promise<TWebSocket>}
710
797
  */
711
798
  async _connect() {
712
- if (this.IsConnected()) {
713
- return Promise.reject(new Error(ALREADY_CONNECTED))
714
- }
799
+ if (this.IsConnected()) throw new Error(ALREADY_CONNECTED)
800
+
801
+ if (this._startPromise) return this._startPromise
802
+
803
+ if (this._ws) return this._ws
715
804
 
716
- this._ws = new TWebSocket(this._options)
805
+ const generation = this._generation,
806
+ ws = new TWebSocket(this._options)
807
+
808
+ this._ws = ws
809
+
810
+ const current = () => generation === this._generation && this._ws === ws
811
+
812
+ ws.addErrorListener((e) => {
813
+ if (!current()) return
717
814
 
718
- this._ws.addErrorListener(e => {
719
815
  this._setConnected(false)
720
- this._setSubscribed(false)
721
- this._setSubscribeAddresses([], false)
722
- this._setSubscribeSignedData({})
816
+ this._clearSubscription()
723
817
  this._callErrorCallback(e)
724
818
  })
819
+ ws.addOpenListener((e) => {
820
+ if (!current()) return
725
821
 
726
- this._ws.addOpenListener(e => {
727
822
  this._setConnected(true)
728
823
  this._callSuccessCallback(e)
729
824
  })
730
-
731
- this._ws.addMessageListener(message => {
732
- this._callListenCallback(message.data)
825
+ ws.addMessageListener((message) => {
826
+ if (current()) this._callListenCallback(message.data)
733
827
  })
828
+ ws.addCloseListener((e) => {
829
+ if (!current()) return
734
830
 
735
- this._ws.addCloseListener(e => {
736
831
  this._setConnected(false)
737
- this._setSubscribed(false)
738
- this._setSubscribeAddresses([], false)
739
- this._setSubscribeSignedData({})
832
+ this._clearSubscription()
833
+
834
+ if (!ws.active) this._ws = null
835
+
740
836
  this._callCloseCallback(e)
741
837
  })
742
838
 
743
- return this._ws.connect()
839
+ const start = ws.connect()
840
+
841
+ this._startPromise = start
842
+
843
+ try {
844
+ return await start
845
+ } catch {
846
+ if (current()) await this._disconnect()
847
+
848
+ throw new Error('Connection failed')
849
+ } finally {
850
+ if (this._startPromise === start) this._startPromise = null
851
+ }
744
852
  }
745
853
 
746
- /**
747
- * @param {?number} code
748
- * @return {Promise<TCaBCIClient>}
749
- */
750
854
  async _disconnect(code = 1000) {
751
- if (this.IsConnected()) {
752
- await this._ws.disconnect(code)
855
+ code = closeCode(code)
753
856
 
754
- this._setConnected(false)
755
- this._setSubscribed(false)
756
- }
857
+ const ws = this._ws
858
+
859
+ this._generation++
860
+ this._ws = null
861
+ this._startPromise = null
862
+ this._setConnected(false)
863
+ this._clearSubscription()
864
+ await ws?.disconnect(code)
757
865
 
758
866
  return this
759
867
  }
760
868
 
869
+ _clearSubscription() {
870
+ this._setSubscribed(false)
871
+ this._setSubscribeAddresses([])
872
+ this._setSubscribeSignedData({})
873
+ }
874
+
875
+ /**
876
+ * Stop all work and release application callback references.
877
+ * @returns {Promise<TCaBCIClient>}
878
+ */
879
+ async Dispose() {
880
+ await this.Stop()
881
+ this._successCb = null
882
+ this._errorCb = null
883
+ this._closeCb = null
884
+ this._listenCb = null
885
+
886
+ return this
887
+ }
888
+
889
+ _request(uri, req, options = {}) {
890
+ const readOnly =
891
+ req.method === 'GET' ||
892
+ ['/v1/tx_summary', '/v1/tx_search/p', '/v1/bulk_tx'].includes(uri)
893
+
894
+ return this._httpClient.request(uri, req, { ...options, retry: readOnly })
895
+ }
896
+
761
897
  _setSubscribed(value) {
762
898
  this._subscribed = value
763
899
  }
@@ -773,11 +909,14 @@ export default class TCaBCIClient {
773
909
  */
774
910
  _setSubscribeAddresses(addresses, push = false) {
775
911
  if (push) {
776
- this._subscribedAddresses.push(...addresses)
912
+ this._subscribedAddresses = [
913
+ ...new Set([...this._subscribedAddresses, ...addresses])
914
+ ]
777
915
 
778
916
  return this
779
917
  }
780
- this._subscribedAddresses = addresses
918
+
919
+ this._subscribedAddresses = [...addresses]
781
920
 
782
921
  return this
783
922
  }
@@ -787,7 +926,7 @@ export default class TCaBCIClient {
787
926
  * @return {TCaBCIClient}
788
927
  */
789
928
  _setSubscribeSignedData(signedData) {
790
- this._SubscribedSignedData = signedData
929
+ this._SubscribedSignedData = { ...signedData }
791
930
 
792
931
  return this
793
932
  }
@@ -808,41 +947,52 @@ export default class TCaBCIClient {
808
947
  let msg
809
948
 
810
949
  const { transaction, error: e1 } = Transaction.FromJSON(message)
950
+
811
951
  if (e1) {
812
952
  const { message: _msg, error: e2 } = Message.FromJSON(message)
953
+
813
954
  if (e2) {
814
955
  this._callErrorCallback(e2)
956
+
815
957
  return
816
958
  }
817
959
 
818
- switch (_msg.type) {
960
+ switch (_msg.Type) {
819
961
  case MBlock:
820
- msg = Block.FromObject(_msg.data)
962
+ msg = Block.FromObject(_msg.Data)
963
+
821
964
  if (msg.error) {
822
965
  this._callErrorCallback(msg.error)
966
+
823
967
  return
824
968
  }
825
969
 
826
970
  if (this._listenCb) this._listenCb(msg.block, null, null)
971
+
827
972
  break
828
973
  case MTransaction:
829
- msg = Transaction.FromObject(_msg.data)
974
+ msg = Transaction.FromObject(_msg.Data)
975
+
830
976
  if (msg.error) {
831
977
  this._callErrorCallback(msg.error)
978
+
832
979
  return
833
980
  }
834
981
 
835
982
  if (this._listenCb) this._listenCb(null, msg.transaction, null)
983
+
836
984
  break
837
985
  case Subscription:
838
- if (message.state === OK) {
986
+ if (_msg.State === OK) {
839
987
  this._setSubscribed(true)
840
988
  } else {
841
- this._setSubscribed(false)
989
+ this._clearSubscription()
842
990
  }
991
+
843
992
  break
844
993
  default:
845
994
  if (this._listenCb) this._listenCb(null, null, _msg)
995
+
846
996
  break
847
997
  }
848
998
  } else {